From 2dc1f8f2e3013170ddc31c64009b03b402d0954b Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Thu, 16 May 2024 22:02:57 +0200 Subject: [PATCH 01/14] Fix for issue #114: Added support for device HmIP-SWO-B --- .gitignore | 4 + app.json | 88 ++++++++++++++++++++ cli/cmd/bindata.go | 53 +++++++----- drivers/HmIP-SWO-B/assets/icon.svg | 90 +++++++++++++++++++++ drivers/HmIP-SWO-B/assets/images/large.png | Bin 0 -> 21443 bytes drivers/HmIP-SWO-B/assets/images/small.png | Bin 0 -> 1311 bytes drivers/HmIP-SWO-B/device.js | 41 ++++++++++ drivers/HmIP-SWO-B/driver.compose.json | 88 ++++++++++++++++++++ drivers/HmIP-SWO-B/driver.js | 24 ++++++ 9 files changed, 366 insertions(+), 22 deletions(-) create mode 100644 drivers/HmIP-SWO-B/assets/icon.svg create mode 100644 drivers/HmIP-SWO-B/assets/images/large.png create mode 100644 drivers/HmIP-SWO-B/assets/images/small.png create mode 100644 drivers/HmIP-SWO-B/device.js create mode 100644 drivers/HmIP-SWO-B/driver.compose.json create mode 100644 drivers/HmIP-SWO-B/driver.js diff --git a/.gitignore b/.gitignore index d9228bd..e83e955 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ package-lock.json .idea /node_modules/ /cli/homeymatic-cli + + +# Added by Homey CLI +/.homeybuild/ \ No newline at end of file diff --git a/app.json b/app.json index 2bb2ab3..285c166 100644 --- a/app.json +++ b/app.json @@ -9147,6 +9147,94 @@ } ] }, + { + "id": "HmIP-SWO-B", + "name": { + "en": "HmIP-SWO-B" + }, + "class": "sensor", + "capabilities": [], + "images": { + "large": "/drivers/HmIP-SWO-B/assets/images/large.png", + "small": "/drivers/HmIP-SWO-B/assets/images/small.png" + }, + "pair": [ + { + "id": "list_bridges", + "template": "list_devices", + "options": { + "singular": true + }, + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "settings": [ + { + "type": "group", + "label": { + "en": "Device data" + }, + "children": [ + { + "id": "app", + "type": "label", + "label": { + "en": "App" + }, + "value": "Homematic" + }, + { + "id": "driver", + "type": "label", + "label": { + "en": "Driver" + }, + "value": "", + "hint": { + "en": "In most cases the driver name is the same as the device type, but some drivers support multiple device types. In those cases they might not match." + } + }, + { + "id": "address", + "type": "label", + "label": { + "en": "Device Address" + }, + "value": "" + }, + { + "id": "ccuIP", + "type": "label", + "label": { + "en": "CCU IP" + }, + "value": "" + }, + { + "id": "ccuSerial", + "type": "text", + "label": { + "en": "CCU Serial" + }, + "value": "" + } + ] + } + ] + }, { "id": "HmIP-SWO-PR", "name": { diff --git a/cli/cmd/bindata.go b/cli/cmd/bindata.go index 16e6b61..c073bc6 100644 --- a/cli/cmd/bindata.go +++ b/cli/cmd/bindata.go @@ -1,12 +1,10 @@ -// Code generated by go-bindata. +// Code generated for package cmd by go-bindata DO NOT EDIT. (@generated) // sources: // data/driver/assets/icon.svg // data/driver/device.js // data/driver/driver.compose.json // data/driver/driver.flow.compose.json // data/driver/driver.js -// DO NOT EDIT! - package cmd import ( @@ -53,26 +51,37 @@ type bindataFileInfo struct { modTime time.Time } +// Name return file name func (fi bindataFileInfo) Name() string { return fi.name } + +// Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } + +// Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } + +// Mode return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } + +// IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { - return false + return fi.mode&os.ModeDir != 0 } + +// Sys return file is sys mode func (fi bindataFileInfo) Sys() interface{} { return nil } -var _dataDriverAssetsIconSvg = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x56\x4f\x73\xeb\xc6\x0d\xbf\xfb\x53\x6c\x99\x4b\x32\x25\xa1\x05\xf6\x1f\xe0\x5a\xce\x4c\xfb\x9a\x49\x0e\xb9\xb4\x69\x3a\x93\x1b\x1f\x49\x4b\x9c\x27\x89\x1a\x92\x96\xec\xf7\xe9\x3b\xbb\x24\x25\xd9\x7e\xed\x94\x17\x62\x7f\xf8\xb3\xc0\x6f\x81\x25\x1f\x7e\x7c\xd9\xef\xd4\xa9\xe9\x87\xb6\x3b\xac\x33\x04\x9d\xa9\xe6\x50\x75\x75\x7b\xd8\xac\xb3\x7f\xfd\xf6\x53\xc1\x99\x1a\xc6\xf2\x50\x97\xbb\xee\xd0\xac\xb3\x43\x97\xfd\xf8\x78\xf7\xf0\xa7\xa2\x50\x7f\xeb\x9b\x72\x6c\x6a\x75\x6e\xc7\xad\xfa\xe5\xf0\x65\xa8\xca\x63\xa3\xbe\xdf\x8e\xe3\xf1\x7e\xb5\x3a\x9f\xcf\xd0\xce\x20\x74\xfd\x66\xf5\x83\x2a\x8a\xc7\xbb\xbb\x87\xe1\xb4\xb9\x53\x4a\xbd\xec\x77\x87\xe1\xbe\xae\xd6\xd9\xec\x70\x7c\xee\x77\xc9\xb0\xae\x56\xcd\xae\xd9\x37\x87\x71\x58\x21\xe0\x2a\xbb\x9a\x57\x57\xf3\x2a\xee\xde\x9e\x9a\xaa\xdb\xef\xbb\xc3\x90\x3c\x0f\xc3\x77\x37\xc6\x7d\xfd\x74\xb1\x8e\xd9\x9c\x4d\x32\x42\x11\x59\x69\x5a\x11\x15\x7d\xfd\x54\x0c\xaf\x87\xb1\x7c\x29\xde\xba\x0e\xa7\xcd\xb7\x5c\x49\x6b\xbd\x1a\x4e\x9b\xab\xe5\xff\x67\x75\x3f\x74\x75\x7b\xec\xea\xf6\x62\xbe\x00\x30\x74\xcf\x7d\xd5\x3c\x75\xfd\xa6\x81\x43\x33\xae\x3e\xfd\xf6\xe9\xa2\x2c\x34\xd4\x63\x7d\x13\x66\xe1\xf3\xcd\xae\x6f\x48\x3e\x94\xfb\x66\x38\x96\x55\x33\xac\x16\x3c\xf9\x9f\xdb\x7a\xdc\xae\x33\xa7\x75\x5a\x6e\x9b\x76\xb3\x1d\xaf\xeb\x53\xdb\x9c\xff\xda\xbd\xac\x33\xad\xb4\x42\x43\x40\x82\xde\x5f\xa4\x30\x19\x5d\xbb\x04\x13\xd0\xd6\xeb\x6c\x38\x6d\x78\x5a\xcc\xdb\xdd\x5f\xcc\x34\x08\x81\x55\xdf\xbb\xba\xf4\x2c\x95\x41\x93\x2b\xd2\x28\x85\xc6\x02\xed\x0f\xc9\x6b\x29\xf5\xbe\xee\xaa\x98\xfb\x3a\x6b\xab\xee\x00\x91\xbd\xc7\x3b\xa5\x1e\xea\xe6\x69\x88\x76\xd3\x66\x71\x45\x99\x5a\x25\xd5\xc5\x35\xfa\xd5\xb1\x82\xab\xe1\xe7\x72\x98\xea\x56\xea\x58\x6e\x9a\xaa\xdb\x75\xfd\x3a\xfb\xee\x29\x3d\xb3\xe2\x73\xd7\xd7\x4d\xbf\xa8\x7c\x7a\xde\xa8\xba\x63\x59\xb5\xe3\xeb\x34\x15\x73\xec\xa5\xc8\x18\xf5\xa2\xd7\xdf\xd6\x0f\xdb\xb2\xee\xce\xeb\x8c\xde\x2b\xbf\x76\xdd\x3e\x46\x35\x2c\xec\x49\xde\xab\xab\x97\x75\x16\x2c\x88\x21\xf2\xf6\x83\xf2\x75\x9d\x11\x23\xb0\xb5\x1c\xde\x2b\xeb\xae\x7a\x8e\x73\x53\x3c\x1f\xda\x71\x58\x67\xfb\xfd\x07\xf7\xe7\xbe\x8f\x06\xbb\xf2\xb5\xe9\xd7\x59\x7a\xe1\x6c\x34\x6c\xbb\xf3\xa6\x8f\xf4\x3d\x95\xbb\x0b\x7f\x73\xa8\xe3\xcb\xfb\x50\xe7\xf6\x50\x77\xe7\x62\xee\x2c\x14\xfa\x40\xc2\x6c\xb1\x34\x1b\x6a\xfc\x90\xf1\x6c\xf2\xb2\xce\x0a\xfe\x2f\xba\xd7\xff\xa1\xdb\x97\x2f\xed\xbe\xfd\xda\xd4\xeb\x0c\x97\xbe\xd8\x37\x63\x59\x97\x63\x79\xed\x86\x05\x71\xa9\xa7\x94\x7a\xe8\xeb\xa7\xfb\x7f\x7c\xfa\x69\x5a\x29\xf5\x50\x55\xf7\xff\xee\xfa\x2f\xf3\x52\x29\x15\x0d\xca\xcf\xdd\xf3\xb8\xce\xb2\xc7\x0b\xfc\x50\x57\xf7\x4f\x5d\xbf\x2f\xc7\xc7\x76\x5f\x6e\x9a\x38\xe4\x7f\x7e\xd9\xef\x1e\x56\x57\xc5\x1b\xe3\xf1\xf5\xd8\x5c\x83\x4e\x61\xfb\x66\x1a\xf9\x6f\xde\x7b\x75\xb5\x6f\xa3\xd3\xea\x9f\x63\xbb\xdb\xfd\x12\x37\x99\xcb\xba\x09\xda\x8e\xbb\xe6\x31\xed\x39\x89\x4b\x15\xab\xb9\x8c\xb9\xc8\xd5\x4d\x95\x0f\xab\x85\x83\xb4\xda\xbc\x63\x73\x57\x7e\x6e\x76\xeb\xec\xef\x9f\x9b\x43\xa3\xf0\x3d\xd7\x9b\xbe\x7b\x3e\xee\xbb\xba\x99\xfb\x25\xbb\x32\xfb\xa6\x7f\xc6\xbe\x3c\x0c\x91\x86\x75\x96\xc4\x5d\x39\x36\xdf\xeb\xbc\x40\x6f\x21\x68\x36\xf4\xc3\xc2\xff\xb1\x1c\xb7\x4b\x4d\xc3\xf8\xba\x6b\xd6\xd9\x53\xbb\xdb\xdd\x7f\xa7\xd3\xf3\x97\x61\xec\xbb\x2f\xcd\xd4\x5a\xf7\x1a\x5c\x30\x3a\x38\x5c\x9a\x40\xa9\x78\xa6\x0a\x2d\xa0\x38\x6f\x42\x4e\xc6\x80\x71\xce\x92\x3a\xa9\xc2\x69\x70\x12\xd8\xa8\x9d\x22\xb0\x5e\x5b\xa2\xbc\x20\xb0\x68\x03\x2d\x08\x5e\x11\x87\x40\xde\x19\xce\x35\xa0\xf5\x26\x7c\x04\xd2\xdb\x52\xee\x34\x58\xed\x3e\xac\x51\xcd\xb1\x38\x9f\xa2\x87\x05\x08\x33\xc0\xea\x67\xe5\x1d\x78\x67\x34\xdb\x6b\xda\xea\x0f\xf5\xab\x42\x74\x20\xe8\x2d\xe7\xc4\x04\xac\x03\xaa\x4a\x15\x1a\xc8\x60\xa0\xbc\xd0\x60\x82\x15\xab\x0a\x0c\xe0\x58\x93\x33\x11\x8b\x7c\x70\xaa\x14\x43\x60\xe1\x2b\xf6\xb3\x4a\xf1\x2c\x7b\x13\xb9\xb0\x0c\xe2\x6d\x0c\xa9\xf3\xc2\x10\x68\xb2\x2e\xc4\xf0\x28\x4c\x3e\x2f\xac\x80\x66\xef\x8d\x9a\x03\x84\x09\x33\xc8\xc4\x11\x13\xd2\xe2\x52\x74\x2f\x98\x90\x10\xd8\x04\x9f\x4b\x00\x47\xd6\x25\x2e\x0c\x79\xc9\x85\xc1\x05\x34\xa8\x34\x78\xcd\x28\x98\x6b\xf0\xa2\x03\x2b\x06\x0a\xda\xb9\x5c\x03\xa3\x17\x52\x4e\x43\x70\x5a\x42\x24\x98\x4d\x3c\x34\x63\x80\xd0\xe7\x1a\x34\x5a\xa7\xac\x80\xa0\x73\x31\x95\x58\x9f\x66\x13\x21\xcf\xda\xda\x04\x39\x87\x14\xd4\x57\xb5\x57\xf1\xa0\x5d\x70\x79\x21\x0c\x22\x1c\xcc\x4c\x1d\xba\x89\x3a\xe7\x9d\xf3\xaa\x40\x0d\xec\xbc\x49\x75\x04\xf4\xc6\x4e\x3d\x62\x45\xe8\x06\x32\x06\x98\x8d\x71\x94\xeb\xa4\xd7\x1c\x82\x93\xd8\x04\xcc\x82\x2a\xd2\xe2\x25\x10\x9b\x3c\x52\x25\x46\x94\x06\xc3\x86\x7d\x4c\x3c\x0a\x41\x61\x00\x12\x6f\x6c\xac\xdd\x51\xf4\x8a\x27\x84\xce\x52\xc8\x35\x58\x12\x46\xb5\x8b\xc5\x38\xe3\x35\x4d\xf5\x59\x71\xa2\x26\x21\xd8\xdc\x32\x58\xb2\x96\xa7\x42\x34\x93\xcd\xc9\x83\x37\xc6\x7a\xa5\x41\x5b\xef\xa3\x09\x23\xda\xd8\x86\xc4\xde\xd9\xdc\x0a\x10\xdb\x64\x60\xbd\xb5\x36\xd7\x91\x3f\x1b\xf3\x13\x42\x91\xbc\x90\x18\x82\x29\x9e\x96\x75\x1c\x0f\x54\x02\xb0\x23\xc2\x44\xa3\x01\xb2\x88\x2e\x17\x0f\xcc\x2e\x38\x85\xe0\x75\x30\x94\x17\x08\xce\x88\x60\xea\x24\x01\xf6\x14\xec\x45\x8a\xf3\x55\x10\x68\xed\x42\xc8\x11\xc4\x5a\xed\x17\xc0\x5f\x80\x53\x2c\xd7\x3a\x76\x9c\x7a\x90\x02\x90\x26\xa2\xd8\x37\x41\x0c\xe6\x17\xad\x06\x23\x6c\xdd\x2d\x40\x28\xe8\x72\xad\x10\x90\x30\xd8\x48\x97\x17\x12\xaf\x96\x4d\xe6\xfc\x52\x11\x85\x05\x2f\xde\x61\x5e\xd8\x00\xc1\x5a\xe7\xd5\xef\x0a\xd9\x46\xb2\xc8\xc5\x01\xf4\xa0\x1d\x8b\xa5\x78\x4c\xda\xa3\xc6\x38\x21\x96\x01\x0d\x1b\xbc\x0a\xdb\x28\x8a\x04\x21\xb9\x48\x3c\x6d\xc1\x0e\xc4\x23\xa5\xce\x4d\xdc\x47\x62\x8c\x07\x23\x9a\x82\xda\x2a\xe3\x41\xbc\x31\x6e\x11\x2c\xab\x93\xba\xe8\x2f\xc2\x6d\x2e\x24\xa0\xc5\x39\x21\xf5\x87\xda\xab\x10\xaf\x31\xe3\x89\x73\x1d\xb3\x97\x00\x16\x59\xde\x3b\x78\xcf\xde\x87\x18\xda\x01\x63\xa0\xf9\x8d\x32\xa5\x90\x36\x71\x17\x69\xce\xdd\x73\xbc\x75\xc4\x61\xae\x53\xd6\x08\x64\xc8\x47\x0f\x04\xd6\xce\x07\x73\x23\x9d\xd4\xa2\x9f\xde\xee\x6d\x0e\xc6\x02\xb9\x38\xf5\x29\x69\x1b\x00\x8d\xb3\xc6\xe6\x26\x5e\x5b\x3a\x5d\x60\x0c\x8e\x59\x7c\x3a\x34\x9d\xe6\x87\x08\xbc\x35\xde\x98\x2b\xb4\x04\x8f\xe5\x5e\x3d\x16\x2d\x82\xf3\xe8\x29\x8d\xa0\x27\xcd\xf1\x66\x4b\xc0\x0d\x12\xe2\x05\x28\xd1\x46\x91\x05\xd2\x8e\xe6\x2d\x03\xc9\x8d\x7d\xb1\x38\x7c\x55\xbf\x2a\xf1\x10\x90\xad\x77\xe9\xb3\x41\xc6\xa3\xfa\x5d\x91\x36\x10\x28\x76\xf5\x9b\x42\x1d\x18\x11\x8b\xa2\x4e\x91\x78\x87\xc6\x84\xab\xb0\x55\x46\x83\x77\xe2\xc8\xdc\x48\x91\x6d\x22\xd0\xde\x98\x38\x69\x8e\x20\x56\xcd\x71\xa8\x6c\xc0\x34\x54\x01\x85\x28\x6e\x24\x10\x10\x99\xbc\xc2\xd8\x68\xd6\x4f\x43\x35\x5b\x52\xbe\x58\x2e\x88\x99\x11\xa3\xb6\xe9\x16\xc5\x80\xce\xdf\x48\x5f\x2f\xdf\xc7\xf8\x69\x8e\xdf\x58\x23\xc6\x5e\xc1\xcb\x3f\x60\x77\x38\x34\xd5\xd8\xf5\x45\xf5\xdc\x9f\xca\xf1\xb9\x6f\xd6\x99\x5e\x7e\x9f\x56\x9b\xc7\xbb\x87\xf8\x67\xf3\x78\xf7\x9f\x00\x00\x00\xff\xff\x35\x32\xd3\xa6\x1d\x0e\x00\x00") +var _dataDriverAssetsIconSvg = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x57\x4d\x8f\xe3\xc8\xcd\xbe\x0f\x30\xff\xa1\x5e\xcd\x65\x06\xaf\x44\x17\x59\x5f\x64\xa7\xdd\x0b\x24\x93\xc5\xee\x61\x2f\xc9\x66\x03\xec\x4d\x23\xa9\x6d\x61\x6c\xa9\x21\xa9\xed\xee\xf9\xf5\x41\xe9\xcb\xee\xf1\x6e\x10\x5f\xc4\x7a\xf8\x14\x8b\x7c\x8a\x55\x92\xef\x7f\x78\x39\x1e\xd4\xa9\xea\xfa\xba\x6d\xb6\x09\x82\x4e\x54\xd5\x14\x6d\x59\x37\xbb\x6d\xf2\xaf\x5f\x7f\xcc\x38\x51\xfd\x90\x37\x65\x7e\x68\x9b\x6a\x9b\x34\x6d\xf2\xc3\xc3\xfb\x77\xf7\xff\x97\x65\xea\x6f\x5d\x95\x0f\x55\xa9\xce\xf5\xb0\x57\x3f\x37\x5f\xfb\x22\x7f\xaa\xd4\xc7\xfd\x30\x3c\xdd\x6d\x36\xe7\xf3\x19\xea\x19\x84\xb6\xdb\x6d\x3e\xa9\x2c\x7b\x78\xff\xee\xfd\xbb\xfb\xfe\xb4\x7b\xff\x4e\x29\xf5\x72\x3c\x34\xfd\x5d\x59\x6c\x93\x79\xce\xd3\x73\x77\x18\xb9\x65\xb1\xa9\x0e\xd5\xb1\x6a\x86\x7e\x83\x80\x9b\xe4\x8a\x5f\x5c\xf8\x45\xcc\xa0\x3e\x55\x45\x7b\x3c\xb6\x4d\x3f\x4e\x6d\xfa\x0f\xd7\xec\xae\x7c\x5c\xe9\x31\xa5\xb3\x19\x59\x28\x22\x1b\x4d\x1b\xa2\xac\x2b\x1f\xb3\xfe\xb5\x19\xf2\x97\xec\xbb\xb9\xfd\x69\xf7\x47\x73\x49\x6b\xbd\xe9\x4f\xbb\x2b\xea\xff\x48\xbb\xeb\xdb\xb2\x7e\x6a\xcb\x7a\xe5\x2f\x00\xf4\xed\x73\x57\x54\x8f\x6d\xb7\xab\xa0\xa9\x86\xcd\xe7\x5f\x3f\xaf\xce\x4c\x43\x39\x94\xd7\x71\x16\x61\xdf\xac\xfb\x46\xed\x26\x3f\x56\xfd\x53\x5e\x54\xfd\x66\xc1\xa7\x00\xe7\xba\x1c\xf6\xdb\xc4\x69\x3d\x8d\xf7\x55\xbd\xdb\x0f\x57\xc0\xa9\xae\xce\x7f\x6d\x5f\xb6\x89\x56\x5a\xa1\x21\x20\x41\xef\x57\x2b\xcc\xac\x4b\xcf\xe0\x84\xd4\xe5\x36\xe9\x4f\x3b\x9e\x47\xf3\xa2\x77\x2b\x51\x83\x10\x58\xf5\xd1\x95\xb9\x67\x29\x0c\x9a\x54\x91\x46\xc9\x34\x66\x68\x3f\x4d\xd3\x96\x92\xef\xca\xb6\x88\x25\x6c\x93\xba\x68\x1b\x88\x32\x3e\x44\xc2\x7d\x59\x3d\xf6\x23\x73\x5a\x30\x0e\x29\x51\x9b\xc9\xb9\xce\x8e\x53\xcb\x58\xc8\x15\xf5\x4b\xde\xcf\x12\x28\xf5\x94\xef\xaa\xa2\x3d\xb4\xdd\x36\xf9\xf0\x38\xfe\x16\xcf\x97\xb6\x2b\xab\x6e\xf1\xf9\xf1\xf7\xd6\xd7\x3e\xe5\x45\x3d\xbc\x4e\xa7\x65\x89\xbf\x54\x1b\x03\xaf\x04\xfd\x27\x84\x7e\x9f\x97\xed\x79\x9b\xd0\x8d\xf7\x5b\xdb\x1e\x63\x60\xc3\xc2\x9e\xe4\xc6\x5f\xbc\x6c\x93\x60\x41\x0c\x91\xb7\xb7\xde\xd7\x6d\x42\x8c\xc0\xd6\x72\xb8\xf1\x96\x6d\xf1\x1c\x4f\x54\xf6\xdc\xd4\x43\xbf\x4d\x8e\xc7\xdb\x00\xcf\x5d\x17\x19\x87\xfc\xb5\xea\xb6\xc9\xf8\xc0\x85\xd5\xef\xdb\xf3\xae\x8b\x4a\x3e\xe6\x87\x8b\x94\x73\xb4\xa7\x97\x9b\x68\xe7\xba\x29\xdb\x73\x36\x37\x1c\x0a\xdd\xaa\x31\x53\x96\x1e\x44\x8d\xb7\x79\xcf\x9c\x97\x6d\x92\xf1\x9f\x39\x5f\xff\x9b\xf3\x98\xbf\xd4\xc7\xfa\x5b\x55\x6e\x13\x5c\x5b\xe5\x58\x0d\x79\x99\x0f\xf9\x55\x83\x2c\x90\x9b\x5a\x4d\xa9\xfb\xae\x7c\xbc\xfb\xc7\xe7\x1f\xe7\xa1\x52\xf7\x45\x71\xf7\xef\xb6\xfb\xba\x8c\x95\x52\x91\x92\x7f\x69\x9f\x87\x6d\x92\x3c\x5c\xf0\xfb\xb2\xb8\x7b\x6c\xbb\x63\x3e\x3c\xd4\xc7\x7c\x57\xc5\x8b\xe0\xff\x5f\x8e\x87\xfb\xcd\xc5\xf1\x96\x3d\xbc\x3e\x55\x57\x71\xa7\xc8\x5d\x35\xdd\x0b\x7f\x78\x43\x96\xc5\xb1\x8e\xb3\x36\xff\x1c\xea\xc3\xe1\xe7\xb8\xcc\x52\xde\x55\xd8\x7a\x38\x54\x0f\xe3\xb2\x93\xb9\xd6\xb2\x99\x8b\x59\x8a\xdd\x5c\x57\x7b\xbf\x59\xd4\x98\x86\xbb\xef\xb5\x3d\xe4\x5f\xaa\xc3\x36\xf9\xfb\x97\xaa\xa9\x14\xde\x48\xbf\xeb\xda\xe7\xa7\x63\x5b\x56\x73\x17\x25\x57\x3a\xbf\x6d\xab\xa1\xcb\x9b\x3e\x2a\xb2\x4d\x46\xf3\x90\x0f\xd5\x47\x9d\x66\xe8\x2d\x04\xcd\x86\x3e\xad\xdb\xf1\x94\x0f\xfb\xb5\xba\x7e\x78\x3d\x54\xdb\xe4\xb1\x3e\x1c\xee\x3e\xe8\xf1\xf7\x97\x7e\xe8\xda\xaf\xd5\xd4\x71\x77\x1a\x5c\x30\x3a\x38\x5c\x1b\x43\xa9\xb8\xcb\x0a\x2d\xa0\x38\x6f\x42\x4a\xc6\x80\x71\xce\x92\x3a\xa9\xcc\x69\x70\x12\xd8\xa8\x83\x22\xb0\x5e\x5b\xa2\x34\x23\xb0\x68\x03\x2d\x08\x5e\x10\x87\x40\xde\x19\x4e\x35\xa0\xf5\x26\xdc\x02\xe3\xd3\x52\xea\x34\x58\xed\x6e\xc6\xa8\xe6\x58\x9c\x4e\xd1\xc3\x02\x84\x19\x60\xf5\x93\xf2\x0e\xbc\x33\x9a\xed\x25\x6d\xf5\xbb\xfa\x45\x21\x3a\x10\xf4\x96\x53\x62\x02\xd6\x01\x55\xa1\x32\x0d\x64\x30\x50\x9a\x69\x30\xc1\x8a\x55\x19\x06\x70\xac\xc9\x99\x88\x45\x41\x78\xac\x14\x43\x60\xe1\x0b\xf6\x93\x1a\xe3\x59\xf6\x26\x6a\x61\x19\xc4\xdb\x18\x52\xa7\x99\x21\xd0\x64\x5d\x88\xe1\x51\x98\x7c\x9a\x59\x01\xcd\xde\x1b\x35\x07\x08\x13\x66\x90\x89\x23\x26\xa4\xc5\x8d\xd1\xbd\xe0\x88\x84\xc0\x26\xf8\x54\x02\x38\xb2\x6e\xd4\xc2\x90\x97\x54\x18\x5c\x40\x83\x4a\x83\xd7\x8c\x82\xa9\x06\x2f\x3a\xb0\x62\xa0\xa0\x9d\x4b\x35\x30\x7a\x21\xe5\x34\x04\xa7\x25\x44\x81\xd9\xc4\x4d\x33\x06\x08\x7d\xaa\x41\xa3\x75\xca\x0a\x08\x3a\x17\x53\x89\xf5\x69\x36\x11\xf2\xac\xad\x1d\x21\xe7\x90\x82\xfa\xa6\x8e\x2a\x6e\xb4\x0b\x2e\xcd\x84\x41\x84\x83\x99\xa5\x43\x37\x49\xe7\xbc\x73\x5e\x65\xa8\x81\x9d\x37\x63\x1d\x01\xbd\xb1\x53\x8f\x58\x11\xba\x82\x8c\x01\x66\x63\x1c\xa5\x7a\xf4\x6b\x0e\xc1\x49\x6c\x02\x66\x41\x15\x65\xf1\x12\x88\x4d\x1a\xa5\x12\x23\x4a\x83\x61\xc3\x3e\x26\x1e\x8d\xa0\x30\x00\x89\x37\x36\xd6\xee\x28\xce\x8a\x3b\x84\xce\x52\x48\x35\x58\x12\x46\x75\x88\xc5\x38\xe3\x35\x4d\xf5\x59\x71\xa2\x26\x23\xd8\xd4\x32\x58\xb2\x96\xa7\x42\x34\x93\x4d\xc9\x83\x37\xc6\x7a\xa5\x41\x5b\xef\x23\x85\x11\x6d\x6c\x43\x62\xef\x6c\x6a\x05\x88\xed\x48\xb0\xde\x5a\x9b\xea\xa8\x9f\x8d\xf9\x09\xa1\x48\x9a\x49\x0c\xc1\x14\x77\xcb\x3a\x8e\x1b\x2a\x01\xd8\x11\xe1\x28\xa3\x01\xb2\x88\x2e\x15\x0f\xcc\x2e\x38\x85\xe0\x75\x30\x94\x66\x08\xce\x88\xe0\xd8\x49\x02\xec\x29\xd8\xd5\x8a\xe7\x2b\x23\xd0\xda\x85\x90\x22\x88\xb5\xda\x2f\x80\x5f\x81\x53\x2c\xd7\x3a\x76\x3c\xf6\x20\x05\x20\x4d\x44\xb1\x6f\x82\x18\x4c\x57\xaf\x06\x23\x6c\xdd\x35\x40\x28\xe8\x52\xad\x10\x90\x30\xd8\x28\x97\x17\x12\xaf\x96\x45\xe6\xfc\xc6\x22\x32\x0b\x5e\xbc\xc3\x34\xb3\x01\x82\xb5\xce\xab\xdf\x14\xb2\x8d\x62\x91\x8b\x07\xd0\x83\x76\x2c\x96\xe2\x36\x69\x8f\x1a\xe3\x09\xb1\x0c\x68\xd8\xe0\xc5\xd8\x47\x53\x24\x08\xc9\x6a\xf1\xb4\x04\x3b\x10\x8f\x34\x76\xee\xa8\x7d\x14\xc6\x78\x30\xa2\x29\xa8\xbd\x32\x1e\xc4\x1b\xe3\x16\xc3\xb2\x3a\xa9\xd5\xbf\x1a\xd7\xb9\x90\x80\x16\xe7\x84\xd4\xef\xea\xa8\x42\xbc\xc6\x8c\x27\x4e\x75\xcc\x5e\x02\x58\x64\xf9\x7e\x82\xf7\xec\x7d\x88\xa1\x1d\x30\x06\x9a\x9f\x28\x53\x0a\xe3\x22\x6e\xb5\xe6\xdc\x3d\xc7\x5b\x47\x1c\xa6\x7a\xcc\x1a\x81\x0c\xf9\x38\x03\x81\xb5\xf3\xc1\x5c\x59\x27\xb5\xf8\xa7\xa7\x7b\x9b\x83\xb1\x40\x2e\x9e\xfa\x31\x69\x1b\x00\x8d\xb3\xc6\xa6\x26\x5e\x5b\x7a\xbc\xc0\x18\x1c\xb3\xf8\x71\xd3\xf4\x78\x7e\x88\xc0\x5b\xe3\x8d\xb9\x40\x4b\xf0\x58\xee\x65\xc6\xe2\x45\x70\x1e\x3d\x8d\x47\xd0\x93\xe6\x78\xb3\x8d\xc0\x15\x12\xe2\x05\x28\x91\xa3\xc8\x02\x69\x47\xf3\x92\x81\xe4\x8a\x9f\x2d\x13\xbe\xa9\x5f\x94\x78\x08\xc8\xd6\xbb\xf1\xb5\x41\xc6\xa3\xfa\x4d\x91\x36\x10\x28\x76\xf5\x9b\x42\x1d\x18\x11\x8b\xa2\x4e\x51\x78\x87\xc6\x84\x8b\xb1\x57\x46\x83\x77\xe2\xc8\x5c\x59\x51\x6d\x22\xd0\xde\x98\x78\xd2\x1c\x41\xac\x9a\xe3\xa1\xb2\x01\xc7\x43\x15\x50\x88\xe2\x42\x02\x01\x91\xc9\x2b\x8c\x8d\x66\xfd\x74\xa8\x66\x26\xa5\x0b\x73\x41\xcc\x8c\x18\xb5\x1f\x6f\x51\x0c\xe8\xfc\x95\xf5\xed\xf2\x82\x8c\xef\xe7\xf8\x9a\x35\x62\xec\x15\xba\x7e\x20\xb6\x4d\x53\x15\x43\xdb\x65\xc5\x73\x77\xca\x87\xe7\xae\xda\x26\x7a\xfd\xa8\xda\xec\xe2\x7f\xc3\xf8\xad\xf3\xf0\xfe\xdd\x7f\x02\x00\x00\xff\xff\x70\x54\x97\xe5\x5c\x0e\x00\x00") func dataDriverAssetsIconSvgBytes() ([]byte, error) { return bindataRead( @@ -87,12 +96,12 @@ func dataDriverAssetsIconSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "data/driver/assets/icon.svg", size: 3613, mode: os.FileMode(420), modTime: time.Unix(1604862634, 0)} + info := bindataFileInfo{name: "data/driver/assets/icon.svg", size: 3676, mode: os.FileMode(438), modTime: time.Unix(1715717688, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _dataDriverDeviceJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x54\x4d\x6f\xdb\x30\x0c\xbd\xfb\x57\x10\xbe\x38\x46\x52\x75\xbd\xc6\xf3\x80\x6e\xcd\xd6\x02\xfd\x42\x93\xfb\x20\xdb\x4c\xa2\x4d\x91\x32\x89\xce\x92\x19\xfa\xef\x83\x1c\x3b\x75\x3e\xd0\xf6\xb2\x0f\x9f\x04\x3e\x52\x7c\xe2\x7b\x66\x54\x5a\x04\x4b\x46\xe4\x14\x25\x41\x90\x6b\x65\x09\xae\xf5\x02\x37\x90\x82\xc1\x1f\xa5\x30\xd8\x8b\xe6\x3e\x10\xc5\x49\x83\x5f\xe1\x4a\xe4\xd8\x4d\x60\xec\x9c\xb1\x73\x29\xb2\xf3\xa2\xc6\xd8\x37\xeb\xd3\x83\xaa\x02\x31\x05\xa5\x09\xd8\x5d\x29\x49\x34\x95\x67\xce\x05\x55\x75\xe6\x31\xf6\x89\x2f\x79\x26\xa4\x20\x81\xb6\x06\xb6\x3d\xf2\x36\xbc\xb9\xe3\x4b\x48\xa1\x0a\x00\x00\x7c\x91\xe1\x6a\x86\x20\x94\x20\xc1\xe5\x41\xbd\x73\x75\x5a\x58\x55\xcc\xb9\x70\xd8\x54\xd5\xa1\x7c\xce\x95\x42\x19\x0e\xe1\x62\xf0\x1c\xfd\x8e\x9b\x70\x08\xe1\x78\x72\x39\x19\x85\x9d\xf8\x8a\xcb\x12\x27\x9b\x25\x7a\x34\xd3\x5a\x22\x57\x61\x0d\xbb\xc1\x8e\x09\xaa\xa2\xd3\x51\x72\x4b\xfb\x74\xfe\x18\x85\xc0\xd5\xa3\x45\x69\xf1\xa5\x91\x35\x59\xaa\xd8\x0d\xbc\x3d\xe7\x92\x5b\x5b\xeb\xbc\xe0\x24\xf2\x46\x16\x5c\x13\xaa\xc2\xb6\xfa\x56\x41\xdd\x4d\xab\x1b\x25\xa8\x17\x77\x5e\xb2\xe2\x06\x44\xb1\x86\x14\x68\x2e\x2c\x9b\x21\x5d\x71\xe2\xbd\x98\x71\x22\x23\xb2\x92\xd0\xb2\x1b\x55\xe0\x3a\xd9\x95\xb4\x72\x77\x6d\xd0\xcc\x6e\x0b\x9f\x36\x43\xb7\xe3\x69\x4b\x74\x1b\xbc\xc1\x1a\xbb\xf1\x1e\x5b\xe4\x65\x9d\x5e\xd3\xeb\x0d\xba\xb5\x9f\x1b\x1c\x31\xef\x58\xa9\xc3\xf0\x55\x4b\xfd\x3d\xca\xc1\xf1\xa9\xe6\xed\x2d\xf8\x9a\x4e\x07\x25\xfb\x4f\x3d\x11\xb2\xe5\x12\x0d\x6b\x7c\xb7\x77\x5b\x9c\x34\x3f\x40\xbb\x3e\x14\x02\xbb\x2f\x17\x19\x9a\x8f\x25\x91\x56\x16\xde\xf9\x9b\xea\xac\xc6\x07\xe2\x17\x8e\xd6\x64\xf8\x68\x85\x8a\x6e\x85\x25\x54\x68\xec\x91\x9f\x2d\xca\x69\x63\xe8\x67\xd7\x4e\xb5\x81\x9e\x44\x82\xac\xbe\x1d\x52\xb8\x48\xda\xf3\xfb\xd4\xbb\xf6\xa0\xbb\x73\x2d\xde\xef\xc7\x07\x42\xf9\x0e\x2c\x33\xa2\x98\x21\xd3\xaa\x17\xa1\x27\x74\x16\x41\x7f\x8b\xd4\x5b\xf7\x46\x11\x9a\x29\xcf\xf1\x9e\x2f\x10\xfa\x10\x3d\xe3\xdb\xcd\x7a\x59\x14\x06\xad\xf5\xd0\xd0\x43\x0d\x19\x9f\xf9\xf8\x34\x1a\x8f\xbf\x8e\xaf\x1f\x9e\x26\xd1\x00\x7a\xb5\xa8\x31\xa4\x1f\x4e\xf8\x65\x7b\xa1\x11\x2b\x34\x8c\x8c\x98\xcd\xda\x17\x3c\xfa\xcb\xb1\xf8\x2c\xf5\xcf\x9e\x4f\x1a\x40\x05\xe1\xb6\x47\x38\x6c\x9b\xb9\x53\xd1\x01\x84\x4b\x5f\xdc\xda\xc8\xce\xb5\xa1\x10\x5c\xbc\xef\xa3\x38\xf9\x07\x33\xb9\x7d\xb8\xff\xf2\x1f\x8c\x44\x6a\x35\x7b\x71\x22\xae\x71\x77\xe7\x9f\xf0\xab\x7e\xa1\x8b\x52\x22\xc3\xf5\x52\x1b\xb2\x90\x1e\x2e\xee\x24\xf8\x1d\x00\x00\xff\xff\x5c\xf8\xbf\xae\xc7\x07\x00\x00") +var _dataDriverDeviceJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x94\x5d\x6f\xda\x3c\x14\xc7\xef\x91\xf8\x0e\x47\xb9\x09\x11\xe0\x3e\xbd\x25\x4f\x26\x75\x2b\x5b\x2b\xf5\x4d\x85\xfb\xc9\x49\x4e\xc1\x9b\xb1\x99\x7d\xc2\x60\x51\xbe\xfb\xe4\xbc\x40\x9a\xd2\xd1\x9b\x4e\xeb\x55\x73\xfe\xe7\xcd\xfe\xff\xb0\x9f\x59\x04\x4b\x46\x24\xe4\x87\xfd\x5e\xbf\x97\x68\x65\x09\xae\xf4\x0a\x77\x10\x81\xc1\x1f\x99\x30\x38\xf0\x97\x2e\xe0\x07\x61\x93\x70\x89\x1b\x91\x60\x3b\x83\xb1\x33\xc6\xce\xa4\x88\xcf\xd2\x52\x63\xdf\x6c\x99\xdf\xef\xe5\x39\x88\x27\x50\x9a\x80\xdd\x66\x92\x44\x5d\x3b\x2e\x0a\xa7\x8d\x9d\xc8\x3e\xf1\x35\x8f\x85\x14\x24\xd0\x56\x4a\x35\x27\x69\xe2\xbb\x5b\xbe\x86\x08\xf2\x7e\x0f\x00\xc0\x95\x19\xae\x16\x08\x42\x09\x12\x5c\x76\x3a\xb8\x06\x2e\xcf\xcb\x73\x56\x14\xde\xa4\xa9\x2b\x63\xc9\x92\x2b\x85\xd2\x9b\xc0\xf9\xa8\x15\xfe\x8e\x3b\x6f\x02\xde\x6c\x7e\x31\x9f\x7a\x6d\x61\xc3\x65\x86\xf3\xdd\x1a\x9d\x1c\x6b\x2d\x91\x2b\xaf\xd2\x8b\xd1\x61\x1f\x54\x69\x7b\xae\xe4\x96\x9e\x6f\xf5\xae\x8b\xf4\x7b\x45\x7d\xd7\x28\x2d\xfe\xf1\x0a\xf7\x89\x2a\x3d\x98\xb0\xff\x48\x24\xb7\xb6\x04\x60\xc5\x49\x24\xb5\x59\xb8\x25\x54\xa9\x6d\x7c\xcf\x5d\x0b\x37\x57\xab\x6b\x25\x68\x10\xb4\xcf\xb5\xe1\x06\x44\xba\x85\x08\x68\x29\x2c\x5b\x20\x5d\x72\xe2\x83\x80\x71\x22\x23\xe2\x8c\xd0\xb2\x6b\x95\xe2\x36\x3c\xd4\x34\x1c\xb4\x01\x69\x2e\xb3\xd2\x5f\xc1\xa4\x3d\xf4\x15\x58\xda\x33\xde\x02\xcd\xfe\xbe\x8f\xc0\x73\xc2\xbb\x93\x1e\xbe\xc5\xcb\xe6\xaf\x18\xbd\xdc\xbf\x0d\x59\x6b\xcf\xd3\xb0\xfd\xd5\xc5\x0f\x9f\xc5\x73\x8b\x4b\x36\x4f\xda\xd6\x2d\xea\x9c\xf9\x58\xcc\x66\x6b\x34\xac\xa6\xf1\x59\xc3\x20\xdc\xff\x42\xf6\xcf\x8d\x42\x60\x77\xd9\x2a\x46\xf3\x31\x23\xd2\xca\xc2\x7f\x65\xb7\x2a\xb3\xa6\x43\xfc\xc2\xe9\x96\x0c\x9f\x6e\x50\xd1\x8d\xb0\x84\x0a\x8d\x7d\x89\xba\x45\xf9\x54\xb3\xde\x02\xfa\x49\x1b\x18\x48\x24\x88\xcb\x09\x10\xc1\x79\xd8\xfc\xff\x7f\xe4\x78\xee\x6c\x50\x14\x8d\x3e\x1c\x06\x5d\xe7\xdc\x0c\x16\x1b\x91\x2e\x90\x69\x35\xf0\xd1\xed\x34\xf6\x61\x58\x29\xd5\x8b\x7b\x91\xa6\x06\xad\x85\x21\xf8\x13\x27\xd5\xd3\x86\xe0\x8f\x1f\x1e\xa7\xb3\xd9\xd7\xd9\xd5\xfd\xe3\xdc\x1f\xc1\xa0\xf4\x30\x80\xe8\xc3\x31\x42\xaa\x8e\x46\x6c\xd0\x30\x32\x62\xb1\x68\x76\x7c\x70\xdd\x31\xfd\x2c\xf5\xcf\x81\x4b\x1a\x41\x0e\x5e\x35\xc4\x9b\x34\xd3\x8a\x63\xd1\x11\x78\x6b\x57\xdc\x60\x63\x97\xda\x90\x07\x45\xd0\xe1\x26\x08\xdf\xe3\xd8\x37\xf7\x77\x5f\xfe\x85\x53\x4b\xad\x16\x27\x0e\x5d\xec\x59\x6d\x43\x5e\xbf\xed\x2b\x9d\x66\x12\x19\x6e\xd7\xda\x90\x85\xa8\xfb\x48\x87\xfd\xde\xef\x00\x00\x00\xff\xff\xe9\x6e\x77\xbb\xcf\x07\x00\x00") func dataDriverDeviceJsBytes() ([]byte, error) { return bindataRead( @@ -107,12 +116,12 @@ func dataDriverDeviceJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "data/driver/device.js", size: 1991, mode: os.FileMode(420), modTime: time.Unix(1604862634, 0)} + info := bindataFileInfo{name: "data/driver/device.js", size: 1999, mode: os.FileMode(438), modTime: time.Unix(1715717688, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _dataDriverDriverComposeJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x95\x4f\x6b\xe3\x30\x10\xc5\xef\xf9\x14\x83\xcf\x21\xb9\xf7\x56\xd2\xc3\xe6\xb2\x14\x96\x3d\x95\xb2\x4c\x6c\x61\x0f\x48\xb2\xd0\x8c\xc3\x96\xe0\xef\xbe\x48\x72\xfe\xf8\x5f\xc8\x36\xa1\xbd\x05\xe5\xcd\x7b\xef\x27\xd9\xf2\x61\x01\x00\x90\x51\x91\x3d\x41\x76\x38\xc0\xea\xc5\xd3\x5e\xf9\x9f\x68\x14\xb4\x6d\xb6\x4c\x7f\x5b\x34\x2a\x7b\x82\x24\x8e\x2b\xca\x4e\x0f\x44\x45\xdb\x8d\xe5\x1a\x99\xfb\xba\x4d\x58\xba\x70\xce\xd1\xe1\x8e\x34\x09\xa9\xa0\x7c\x7b\xef\xd6\xc9\x60\x19\x57\x2e\x32\x35\xfa\x32\xd4\xc8\xd6\x45\xf4\xe2\xf5\x28\x7f\x8d\xcc\x4a\x78\x9d\xc6\xd7\x71\x62\xe5\x6c\xd9\xc5\x45\x1b\x36\xa8\xf5\x7f\xd9\xc4\x89\x68\xd3\xc3\x73\x48\x3e\x74\x3e\x59\x9f\xbb\x5e\x6c\xaa\x26\x96\x3f\x3b\x4f\x45\xe0\x59\xf6\x15\xa2\x8c\xd3\x28\xea\xa4\x2b\xd4\x9e\xf2\xb1\xae\x76\x42\xb5\xed\x6f\xc7\x99\x87\x6c\xd9\x68\x0c\x55\xc4\x37\xaa\x27\x68\x07\x46\x16\xf7\x54\x62\x30\x9b\xf6\xb2\xea\xaf\x8c\xca\xf4\x1d\x17\x13\xde\xf3\xe0\x33\x40\xb7\x82\xdf\xda\x17\x8b\xe2\xde\xba\x97\x16\x57\xda\x4e\x26\xa5\x94\xe3\xb3\xcb\x4a\x84\x6c\xc9\xd7\x9e\x0d\xf9\x70\xd1\xae\xf4\x75\xe3\x86\x79\x1a\x77\x4a\x4f\x03\xa7\xf7\xee\x25\x16\x80\x02\x05\xb3\xab\xe7\x9d\x57\xa4\x0b\x1f\x87\xde\x46\x66\x63\xfb\xcb\xed\x70\xc3\x5a\xa3\xee\xa9\xe6\x8c\x68\x9e\x61\xc0\xf2\xec\x5c\x36\xa9\x69\x67\x9c\xf7\xa8\x9b\x98\xff\xa3\x36\xca\xa0\x50\x3e\x9e\x9f\x98\xbd\x4a\x9b\x6e\x82\xaf\x01\x4e\x37\xcd\x27\x99\xe7\xd2\x2b\xb2\x72\x4b\xf8\xd6\x82\xa9\x59\x20\x47\x56\x0c\x52\x29\x48\xe8\x10\xae\x78\xa0\xb4\xc4\xe1\x37\x76\x7f\xa7\x47\x2d\x6c\xc1\x12\x76\x8d\x00\xd7\xe6\x38\xc4\xc0\x8d\x73\xb5\x17\x30\x8d\x16\x72\xba\x27\xe7\x15\x6c\x2d\x48\x55\xb3\x3a\xc7\x7d\x80\xa1\xb2\x12\xb0\xb5\x80\x41\xc9\xab\xd5\xcc\x46\xdc\x7d\xa4\x58\x14\x5e\xf1\xf0\x5d\x3e\x89\x1e\x7c\xa6\x09\xfb\xb9\xcb\xfc\xe4\xd9\xde\xcd\x9c\xe7\xcd\xf6\xf5\x6b\x88\x37\x9b\xdf\xb0\x7d\xfd\x4e\xd2\x5f\xca\x13\xce\x82\x1c\x69\x25\x7c\x21\x1e\x01\xdb\xc5\x3d\x0c\xb8\xb7\xf2\x3e\xfc\x94\x2c\xda\xc5\xbf\x00\x00\x00\xff\xff\x19\xa1\x48\x70\x9b\x09\x00\x00") +var _dataDriverDriverComposeJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x95\x4f\x6b\xe3\x30\x10\xc5\xef\x81\x7c\x87\xc1\xe7\x90\xdc\x7b\x2b\xe9\x61\x73\x59\x0a\xcb\x9e\x4a\x59\x26\xb6\xb0\x07\x24\x59\x68\xc6\x61\x4b\xc8\x77\x5f\x24\x39\x89\xe3\x3f\x21\x5b\x4c\x7b\x4b\xe4\x37\x6f\xde\x6f\x24\x5b\xc7\xe5\x02\x00\x20\xa3\x22\x7b\x82\xec\x78\x84\xf5\x8b\xa7\x83\xf2\x3f\xd1\x28\x38\x9d\xb2\x55\xfb\xdc\xa2\x51\xd9\x13\xb4\xf2\xb8\xa4\xec\x78\x49\x92\x9c\xce\x95\xb9\x46\xe6\x5b\xe5\x36\x2c\x75\xdd\x73\x74\xb8\x27\x4d\x42\x2a\x48\xdf\xde\xcf\x0f\xc8\x60\x19\x97\xba\x8d\x35\xfa\x32\x84\xc9\x36\x45\xb4\xe3\xcd\x20\xc4\x06\x99\x95\xf0\x26\xd5\x6f\x62\xc5\xda\xd9\xf2\xdc\x31\xfa\xb0\x41\xad\xff\xcb\x27\x56\x44\x9f\x1e\xa4\x43\xf2\x21\xf8\xd5\xbd\x13\xb8\x33\x60\x4d\x2c\x7f\xf6\x9e\x8a\x40\xb5\xea\x49\x44\x19\xa7\x51\xd4\x45\x58\xa8\x03\xe5\x23\xc2\xda\x09\xd5\xb6\x37\x96\x2b\x16\xd9\xb2\xd1\x18\xf2\x88\x6f\xd4\xad\xe2\xd4\xf7\xb2\x78\xa0\x12\x83\xdf\x84\x9d\x55\x7f\x65\x90\xa8\x67\x7a\xfd\xdb\xf5\xbf\x33\x82\x29\xb2\x87\x47\xf0\x70\x6c\x2c\x8a\x39\x52\x77\x6d\xee\x85\x1e\x6f\xd7\xb6\xba\x1c\x6b\x56\x22\x64\x4b\xbe\x7f\x64\xe4\xc3\x45\xcf\xd2\xd7\x8d\x1b\x74\xd5\xb8\x57\x7a\x02\x3e\xbd\x9a\x2f\x31\x07\x14\x28\xd8\x47\xef\x9b\xe5\x15\xe9\xc2\xc7\xb2\xb7\xa1\xdf\x48\x8b\xee\x68\xdc\x20\xdc\x00\x21\x85\x9d\x52\xdd\x41\xe9\x21\x3d\x3b\x97\x8d\x8b\xfa\x48\x97\xc2\x03\xea\x26\x66\xf8\x51\x1b\x65\x50\x28\x1f\x71\x18\xab\xbe\x4f\x9d\x3e\x19\x5f\x06\x9e\xbe\x4a\x9f\x66\x9f\x4c\x50\x91\x95\x87\x02\xec\x2c\x98\x9a\x05\x72\x64\xc5\x20\x95\x82\x34\x01\x08\x37\x03\x50\x5a\xe2\xf0\x1b\xdb\xc7\xe9\xf4\x85\x41\xac\x60\xdf\x08\x70\x6d\xce\x45\x0c\xdc\x38\x57\x7b\x01\xd3\x68\x21\xa7\x6f\xe4\xbc\x86\x9d\x05\xa9\x6a\x56\xd7\x76\x1f\x60\xa8\xac\x04\x6c\x2d\x60\x50\xf2\x6a\x3d\x35\x8c\x39\x76\x17\x8b\xc2\x2b\x1e\xbc\xea\x17\xd5\xec\xdb\x9b\xe8\x9f\xdb\xb6\x9f\xde\xe6\x39\xd8\xf3\xbc\xd9\xbd\x7e\x19\xf9\x76\xfb\x1b\x76\xaf\xdf\x4e\xfc\x4b\x79\xc2\x69\x9e\x33\xb5\x84\x5b\x65\x1e\xe8\xb6\xe3\x9c\xe0\xb7\x4b\xef\xc3\xfb\x67\xb9\x38\x2d\x17\xff\x02\x00\x00\xff\xff\xb1\x5f\x77\x10\xf3\x09\x00\x00") func dataDriverDriverComposeJsonBytes() ([]byte, error) { return bindataRead( @@ -127,12 +136,12 @@ func dataDriverDriverComposeJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "data/driver/driver.compose.json", size: 2459, mode: os.FileMode(420), modTime: time.Unix(1604867793, 0)} + info := bindataFileInfo{name: "data/driver/driver.compose.json", size: 2547, mode: os.FileMode(438), modTime: time.Unix(1715717688, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _dataDriverDriverFlowComposeJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x53\xcd\x6a\xf3\x30\x10\xbc\xfb\x29\x16\x91\x63\x92\x07\x30\x7c\x97\xf0\xf5\x50\x28\xa1\xd0\xde\x4a\x0e\x0a\xde\xba\xa6\xb2\x64\xd6\xb2\x4b\x31\x7a\xf7\x22\xd9\x75\xfc\x27\xdb\x04\x37\xa7\xb0\x9a\x1d\xcd\x8c\xc6\x55\x00\x00\xc0\x34\x25\x71\x8c\x94\xb3\x10\xde\xdc\xc4\xfe\xaa\xf6\x9f\xc3\x24\x11\x0b\x81\x55\x15\x1c\xff\x53\x52\x22\x9d\x79\x8a\x60\xcc\x21\x23\xcc\x73\xb6\xef\x83\x75\xa2\x05\xb2\x70\xc0\xe1\x8e\x50\x5a\x9e\x53\xa1\xb5\x92\xf0\x50\xa2\xd4\xac\x07\x32\x43\x2a\xf5\x89\xb2\xaf\x6c\x5a\x61\xbb\x21\x79\x6a\xef\x66\x57\x77\xc7\x40\xda\x8d\xf7\x3b\x73\x28\x59\xa4\x57\x24\x2f\xca\x6b\x64\xd2\x10\x9b\x84\x99\xd1\xb4\x3f\xb9\x0c\x2c\x73\x8a\xff\xc2\x70\x63\xe5\x57\xea\x42\x2e\x11\xa9\x2c\x52\x5f\x5e\x5c\xc9\x45\x81\xd3\x32\x5b\xb9\xd5\x01\x76\xb5\xa8\x1c\xc2\x7f\x70\x3c\xbb\xac\x4f\xcd\xc4\x8c\x73\xe9\x6e\x12\x97\x31\xc2\xae\xe4\xc2\xee\x3e\x6a\x24\xae\xf1\xc6\x37\xb7\xed\x3d\x81\x5e\x93\x1d\xb7\x31\x1e\x87\x2d\x5e\xf0\x2b\x8a\xd9\x0e\xb4\xd0\xba\x0b\x1d\xea\xd9\x15\xbf\x03\x63\x03\x48\xde\x41\x36\x01\x74\x5c\xef\xed\x11\xca\x68\x29\xbe\x19\xc8\x65\xdc\xc7\x71\x04\x0b\x5d\x73\x9f\xfd\xab\xed\xca\x52\xdd\x9e\x2d\x12\x66\xa1\x1b\x56\x6e\xcd\xd3\xe7\x1f\x8a\xf4\xe6\xaf\xfe\xe2\x58\xef\x7d\x70\xbf\x9a\x55\x96\x84\x92\xf1\xe6\x8e\x9e\x2c\xe9\xbd\x86\xd6\x36\x2f\x98\x3e\xaf\xe7\x97\xc0\x04\x3f\x01\x00\x00\xff\xff\x08\xab\x54\xd2\xa4\x06\x00\x00") +var _dataDriverDriverFlowComposeJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x54\xc1\x6a\xeb\x30\x10\xbc\x07\xf2\x0f\x8b\xc8\x31\xc9\x07\x18\xde\x25\xbc\x1e\x0a\x25\x14\xda\x5b\xc9\x41\xc6\x5b\xd7\x54\x96\x8c\x2c\xbb\x14\xa3\x7f\x2f\x92\x5d\xdb\x51\x2c\xd9\x84\x3a\xa7\x64\x77\x76\x32\x33\x1e\xdc\x6c\x37\x00\x00\x44\xc9\x2c\x4d\x51\x96\x24\x82\xb7\x76\x64\x3e\xcd\xf0\xd5\xa2\xb2\x84\x44\x40\x9a\x06\x8e\xff\x65\x56\xa3\x3c\xd3\x1c\x41\xeb\x43\x21\xb1\x2c\xc9\xde\x41\xab\x4c\x31\x24\x91\xcb\x62\x77\xc8\x0d\xd3\xa9\x52\x4a\x70\x78\xa8\x91\x2b\x72\x8d\xd2\x37\x6c\xe2\x13\xb9\xa3\xcf\xa3\xb3\xbf\xe1\x34\x37\x02\x48\x6c\xff\xc7\x15\x38\x50\x7f\x17\x16\xc6\xab\x3c\x46\xe9\x87\xf9\xfd\x4c\xfa\x22\xd3\x38\x7d\x3b\x76\x46\x17\xd7\x3b\x95\xe9\x4a\xce\x3b\x4b\xbf\x8a\xe7\x12\x4a\xa4\x28\x12\xf1\xe5\x07\xd6\x94\x55\xe8\xd1\xda\x6b\x6e\x0e\xb0\x6b\x85\x95\x10\xfd\x83\xe3\xd9\xe6\x7e\xea\x26\x7a\x22\xa0\xf1\xa9\xa4\x3c\x45\xd8\xd5\x94\x99\xe3\x47\x85\x92\x2a\x1c\x08\x83\xe7\xfe\x15\x5c\xf5\xdb\xb2\x6b\xed\xb3\xd9\x1f\x30\x1a\x23\x0b\x57\xa2\xc7\xb6\xd5\x18\x91\x87\x6f\x02\x36\xb4\x89\x21\x7b\x07\xde\xc5\x30\xf2\xbe\x37\x2b\xe4\xc9\x6c\x8a\x21\xcc\x65\xa2\xa1\x13\x49\xcc\x75\xcf\xbe\x15\x5e\x4d\x73\x66\xeb\xf7\x6c\xa0\x10\xc6\xfe\x69\x05\x17\x15\xa1\xfc\x10\x52\xad\xd0\x81\x17\xcb\x7b\xff\xe3\x0f\x28\x5a\x66\x8c\x09\x9e\xae\xe0\xeb\xc9\xd0\xde\x6f\x6b\x79\x19\x9d\xd7\xe5\xf0\xb3\xdb\x5c\xb6\x1b\xbd\xdd\xfc\x04\x00\x00\xff\xff\x91\x7e\x98\x31\xdc\x06\x00\x00") func dataDriverDriverFlowComposeJsonBytes() ([]byte, error) { return bindataRead( @@ -147,12 +156,12 @@ func dataDriverDriverFlowComposeJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "data/driver/driver.flow.compose.json", size: 1700, mode: os.FileMode(420), modTime: time.Unix(1604862634, 0)} + info := bindataFileInfo{name: "data/driver/driver.flow.compose.json", size: 1756, mode: os.FileMode(438), modTime: time.Unix(1715717688, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _dataDriverDriverJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x54\x41\x6f\xe2\x3c\x10\xbd\xe7\x57\xcc\xa9\x4e\x24\x30\xdf\x1d\xe5\x3b\x6c\xab\xd5\xae\xb4\xaa\xaa\x55\x6f\xab\x6a\x65\x92\x21\x98\x75\x6c\x76\x3c\x81\x56\x51\xfe\xfb\xca\x0e\x81\x50\xa0\xd4\x27\x3c\xf3\xe6\x79\x66\xde\x23\xa2\xf1\x08\x9e\x49\x17\x2c\xe6\x49\x52\x38\xeb\x19\xbe\xb9\x1a\xdf\x20\x07\xc2\xbf\x8d\x26\x4c\xc5\x2a\x04\x44\x36\xdf\xe7\x1f\x48\x6f\x91\xc6\x00\x29\x67\x52\xce\x8c\x5e\xcc\xca\x98\x93\x6b\x1f\xe0\x49\x61\x94\xf7\x91\xaf\x56\xac\x8b\x7d\x21\xbe\x32\xda\xd2\x0f\x3c\x6d\x92\x00\x00\x38\xfb\xdd\x6a\x4e\x33\x68\xe3\x35\x1c\xdf\x6c\x90\xe4\x90\x98\x1f\xe2\x6d\x3b\x05\xbd\x04\x79\xaf\x36\x6a\xa1\x8d\x66\x8d\x1e\xba\xee\x90\xe7\x95\xf6\xb2\x18\x27\x73\xf8\x75\xc8\x0e\x0c\xa4\x6c\x85\xa0\xad\x66\xad\xcc\x75\xb2\x70\x44\xdb\x82\x84\xae\x13\x93\x33\x16\xb4\xe5\x18\xbd\x6f\xcd\x28\xcf\xb7\x29\x2f\xa2\x44\xf2\x01\xfd\xcb\xe9\x12\xd0\x78\xbc\x39\xf8\xcb\x47\x84\xb1\x62\x35\x08\xf4\xfc\xb6\xe9\x6b\xe2\xc0\xbd\x3e\x8f\xaa\x0e\x6f\x88\xd1\xd3\xb1\xc8\xb8\x2a\xbd\x50\x2d\xd7\x4e\xdb\x54\x4c\x44\x36\x01\xb1\x52\x1e\x16\x88\x36\x6e\x19\xcb\xe8\x89\x63\x33\x61\x51\x16\x41\x3e\x36\xf5\x02\xe9\x4b\xc3\xec\xac\x87\xff\x60\xfa\xbe\xbf\xdf\x4b\xe3\x76\xcf\xa4\xab\x6a\x80\x3d\x11\x7a\x8f\x25\xe4\x60\x71\xd7\x1b\x56\x7e\x35\x6e\x77\xaf\xa8\xdc\x03\x1f\x70\xab\x0b\x4c\xcf\x27\x99\x6e\x42\xb1\xc8\x4e\xe4\x90\x84\x95\xf6\x8c\x94\x5e\x89\xff\x6c\xec\x8f\xf0\xc3\x22\xa5\xa9\xa2\xca\x4f\xc0\xb3\x62\xcc\x20\xff\x7f\x64\xd9\xe1\xe8\x25\x44\x94\x5c\xc4\x7e\x21\xcf\x7b\xf8\x70\xbf\xbb\x83\x98\x8e\xbd\x84\xc5\x1d\x11\x87\x50\x76\x81\x37\x1c\x42\x6e\xc8\xc2\x13\xb9\x5a\x7b\x94\x84\xde\x99\x2d\xa6\x4c\x0d\x66\x67\x05\x5d\x6f\x92\x4f\x32\xad\xb1\xe0\x74\xa9\x8c\xbf\xc4\x74\x12\xe9\xb2\x6b\xb6\xea\x7a\x8d\x6f\xeb\xcb\x17\x14\x0d\x22\xa6\x65\x94\x6e\x02\xec\xfe\xa0\x3d\x2e\xba\xfd\xa4\x2b\x4e\x05\xdc\xbf\x72\x85\xf4\x14\x5b\x28\x2e\x56\xbd\xab\x91\xc8\x51\x96\x1c\x07\x1f\x4d\xd9\x25\x49\xed\xca\xc6\xa0\xc4\xd7\x8d\x23\x0e\x7f\x99\x77\x1f\xb9\x79\xf2\x2f\x00\x00\xff\xff\x65\xe6\x70\xfd\x5b\x05\x00\x00") +var _dataDriverDriverJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x54\x4d\x6f\xdb\x30\x0c\xbd\x07\xc8\x7f\xe0\xa9\x92\x81\x44\xd9\x3d\xf0\x0e\x6b\x31\x6c\xc0\x50\x14\x43\x6f\x43\x31\x28\x36\xe3\x28\x93\xa5\x8c\x92\x93\x16\x86\xff\xfb\x20\xd9\xae\x13\x27\xe9\xc2\x93\x4d\x3e\x3e\x7e\x3c\xda\xac\x72\x08\xce\x93\xca\x3c\x5b\x4e\x27\xd3\x49\x66\x8d\xf3\xf0\xcd\x96\xf8\x06\x29\x10\xfe\xad\x14\x21\x67\x9b\xe0\x60\xc9\xb2\x07\x3c\x90\xda\x23\x1d\x23\x84\x58\x08\xb1\xd0\x6a\xb5\xc8\x63\x4c\x6c\x5d\xc4\x4f\x27\x99\x96\xce\x45\xca\x52\x7a\x95\x75\xa9\xf8\xea\xd1\xe4\xae\x67\xaa\x03\x12\x00\xc0\x9a\xef\x46\x79\x9e\x04\x0f\x74\xe6\xaa\x1d\x92\xe8\x23\xcb\x21\x50\xd7\x73\x50\x6b\x10\xf7\x72\x27\x57\x4a\x2b\xaf\xd0\x41\xd3\x0c\x00\xbf\x51\x4e\x64\xc7\xd1\x14\x7e\x0d\xe1\x9e\x83\xa4\x29\x10\x94\x51\x5e\x49\xfd\x01\x5d\x30\x56\xd7\x20\xa0\x69\xd8\xec\x9c\x07\x4d\x7e\x82\xef\xfa\xd3\xd2\xf9\x1b\x58\x2f\xc2\xd8\x29\xdb\xb8\xc2\xcb\x68\x1b\xa8\x1d\xfe\x7f\x03\x2f\x1f\x93\xc6\x9c\x4d\x2f\xd8\xf3\xdb\xae\xcd\x8a\x93\xb7\x7a\x3d\xca\x32\x94\x61\xc7\xe5\x63\x96\xb6\x05\xbf\x90\x2e\xb6\x56\x19\xce\x66\x2c\x99\x01\xdb\x48\x07\x2b\x44\x13\x17\x8e\x79\x77\x26\x43\x43\x61\x65\x06\x41\x3c\x56\xe5\x0a\xe9\x4b\xe5\xbd\x35\x0e\x3e\xc1\xfc\xac\xc7\xdf\x6b\x6d\x0f\xcf\xa4\x8a\xa2\xc7\x3d\x11\x3a\x87\x39\xa4\x60\xf0\xd0\xde\xb1\xf8\xaa\xed\xe1\x5e\x52\xde\x01\x1f\x70\xaf\x32\xe4\xe7\xd3\xcc\x77\x21\x99\x25\xa7\xca\x08\xc2\x42\x39\x8f\xc4\xaf\x05\x7e\x56\xe6\x47\x78\x30\x48\x9c\x4b\x2a\xdc\x0c\x9c\x97\x1e\x13\x48\x3f\x1f\x9f\x71\x6f\x6a\x0d\x11\x26\x56\xb1\x65\x48\xd3\x16\xdf\xbf\xdf\xdd\x41\x0c\xc7\x76\xc2\xfa\x06\xc4\xbb\x2b\xb9\x44\x1c\x8c\xd0\x57\x64\xe0\x89\x6c\xa9\x1c\x0a\x42\x67\xf5\x1e\xb9\xa7\x0a\x93\xf3\x8c\xa6\x3d\x98\x5b\xb9\xb6\x98\x79\xbe\x96\xda\x5d\xe4\x3a\x75\x35\xc9\xf5\x23\x6b\x7a\xc1\x6f\x10\xdb\x5f\x90\x37\x28\xca\xf3\xa8\xe3\x0c\xbc\xfd\x83\x66\xd8\x79\x7d\xeb\x8d\x8c\xd4\xec\xea\x5c\xa1\x1d\x81\x33\xe9\xb3\x4d\x7b\xe8\x48\x64\x29\x79\x9f\x6b\x3c\x6e\x1c\xb5\xb4\x79\xa5\x51\xe0\xeb\xce\x92\x0f\xdf\xd2\xe8\x6f\xb8\x9c\x4e\xfe\x05\x00\x00\xff\xff\xf3\x3e\x47\x31\x8a\x05\x00\x00") func dataDriverDriverJsBytes() ([]byte, error) { return bindataRead( @@ -167,7 +176,7 @@ func dataDriverDriverJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "data/driver/driver.js", size: 1371, mode: os.FileMode(420), modTime: time.Unix(1604862634, 0)} + info := bindataFileInfo{name: "data/driver/driver.js", size: 1418, mode: os.FileMode(438), modTime: time.Unix(1715717688, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -224,11 +233,11 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ - "data/driver/assets/icon.svg": dataDriverAssetsIconSvg, - "data/driver/device.js": dataDriverDeviceJs, - "data/driver/driver.compose.json": dataDriverDriverComposeJson, + "data/driver/assets/icon.svg": dataDriverAssetsIconSvg, + "data/driver/device.js": dataDriverDeviceJs, + "data/driver/driver.compose.json": dataDriverDriverComposeJson, "data/driver/driver.flow.compose.json": dataDriverDriverFlowComposeJson, - "data/driver/driver.js": dataDriverDriverJs, + "data/driver/driver.js": dataDriverDriverJs, } // AssetDir returns the file names below a certain @@ -270,16 +279,17 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } + var _bintree = &bintree{nil, map[string]*bintree{ "data": &bintree{nil, map[string]*bintree{ "driver": &bintree{nil, map[string]*bintree{ "assets": &bintree{nil, map[string]*bintree{ "icon.svg": &bintree{dataDriverAssetsIconSvg, map[string]*bintree{}}, }}, - "device.js": &bintree{dataDriverDeviceJs, map[string]*bintree{}}, - "driver.compose.json": &bintree{dataDriverDriverComposeJson, map[string]*bintree{}}, + "device.js": &bintree{dataDriverDeviceJs, map[string]*bintree{}}, + "driver.compose.json": &bintree{dataDriverDriverComposeJson, map[string]*bintree{}}, "driver.flow.compose.json": &bintree{dataDriverDriverFlowComposeJson, map[string]*bintree{}}, - "driver.js": &bintree{dataDriverDriverJs, map[string]*bintree{}}, + "driver.js": &bintree{dataDriverDriverJs, map[string]*bintree{}}, }}, }}, }} @@ -330,4 +340,3 @@ func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } - diff --git a/drivers/HmIP-SWO-B/assets/icon.svg b/drivers/HmIP-SWO-B/assets/icon.svg new file mode 100644 index 0000000..10d0296 --- /dev/null +++ b/drivers/HmIP-SWO-B/assets/icon.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + diff --git a/drivers/HmIP-SWO-B/assets/images/large.png b/drivers/HmIP-SWO-B/assets/images/large.png new file mode 100644 index 0000000000000000000000000000000000000000..fe7120044b7e4f72f045dac6f595eb25650834eb GIT binary patch literal 21443 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 Date: Sun, 26 May 2024 17:34:00 +0200 Subject: [PATCH 02/14] Optimizations and stability improvements --- .homeycompose/app.json | 5 +- app.json | 3 + lib/HomeMaticCCUJack.js | 12 ++- lib/connection.js | 163 +++++++++++++---------------- lib/convert.js | 82 ++++++--------- lib/device.js | 220 ++++++++++++++++++---------------------- lib/driver.js | 200 ++++++++++++++++++------------------ package.json | 11 +- 8 files changed, 319 insertions(+), 377 deletions(-) diff --git a/.homeycompose/app.json b/.homeycompose/app.json index c2f6f61..514fa6c 100644 --- a/.homeycompose/app.json +++ b/.homeycompose/app.json @@ -48,5 +48,8 @@ "path": "/bridges/delete/", "public": false } - } + }, + "platforms": [ + "local" + ] } \ No newline at end of file diff --git a/app.json b/app.json index 285c166..3bbfc23 100644 --- a/app.json +++ b/app.json @@ -50,6 +50,9 @@ "public": false } }, + "platforms": [ + "local" + ], "drivers": [ { "id": "HM-CC-RT-DN", diff --git a/lib/HomeMaticCCUJack.js b/lib/HomeMaticCCUJack.js index e37044d..60f083c 100644 --- a/lib/HomeMaticCCUJack.js +++ b/lib/HomeMaticCCUJack.js @@ -1,7 +1,7 @@ /*global module:true, require:false*/ const EventEmitter = require('events'); -const axios = require('axios').default; +const axios = require('axios'); const mqtt = require('mqtt'); const Constants = require('./constants.js'); @@ -21,17 +21,19 @@ class HomeMaticCCUJack extends EventEmitter { this.subscribedTopics = []; this.connected = false; this.setupMqtt(user, password); + let clientOptions = { baseURL: 'http://' + ccuIP + ':2121/', timeout: 10000 - } + }; + if (user && password) { clientOptions.auth = { username: user, - password: password - } + password: password + }; } - this.jackClient = axios.create(clientOptions) + this.jackClient = axios.create(clientOptions); } subscribeTopic(name) { diff --git a/lib/connection.js b/lib/connection.js index ab24635..f81585a 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -17,32 +17,31 @@ class InterfaceConnection extends EventEmitter { this.connected = false; this.wasConnected = false; this.reconnectInterval = 10000; - this.setMaxListeners.retrying = false; + this.retrying = false; + freeport((err, port) => { if (err) throw err; this.port = port; - this.initRpcServerAndClient(port) + this.initRpcServerAndClient(port); }); - } getInitUrl() { - return this._settings.protocol + "://" + this._settings.homeyIP + ":" + this.port; + return `${this._settings.protocol}://${this._settings.homeyIP}:${this.port}`; } - retryConnect(self) { - self.logger.log('info', "Check retry: ", self.interfaceName) - if (self.failureCount > self.maxFailureCount && self.wasConnected == false) { - self.logger.log('info', "Giving up on ", self.interfaceName) - clearInterval(self.retryConnectTimer); - self.retrying = false; - return + retryConnect() { + this.logger.log('info', "Check retry: ", this.interfaceName); + if (this.failureCount > this.maxFailureCount && !this.wasConnected) { + this.logger.log('info', "Giving up on ", this.interfaceName); + clearInterval(this.retryConnectTimer); + this.retrying = false; + return; } - if (self.connected == false) { - self.logger.log('info', "Reconecting to ", self.interfaceName) - self.failureCount += 1; - self.initInterface(self.retryConnectTimer); - return + if (!this.connected) { + this.logger.log('info', "Reconnecting to ", this.interfaceName); + this.failureCount += 1; + this.initInterface(); } } @@ -53,31 +52,29 @@ class InterfaceConnection extends EventEmitter { } createRpcServer(port) { - var self = this - this.rpcServer = this._settings.rpc.createServer({ host: this._settings.homeyIP, port: port }); + this.rpcServer = this._settings.rpc.createServer({ host: this._settings.homeyIP, port }); - this.rpcServer.on('system.listMethods', function (err, params, callback) { + this.rpcServer.on('system.listMethods', (err, params, callback) => { callback(null, ['system.listMethods', 'system.multicall', 'event', 'listDevices']); }); - this.rpcServer.on('listDevices', function (err, params, callback) { + this.rpcServer.on('listDevices', (err, params, callback) => { callback(null, []); }); - this.rpcServer.on('event', function (err, params, callback) { - self.emitEvent(params) + this.rpcServer.on('event', (err, params, callback) => { + this.emitEvent(params); callback(null, ''); }); - this.rpcServer.on('system.multicall', function (err, params, callback) { - params[0].forEach(function (call) { + this.rpcServer.on('system.multicall', (err, params, callback) => { + params[0].forEach(call => { if (call.methodName === 'event') { - self.emitEvent(call.params) + this.emitEvent(call.params); } }); callback(null, ''); }); - } createRpcClient() { @@ -86,116 +83,100 @@ class InterfaceConnection extends EventEmitter { emitEvent(event) { if (event && event.length === 4) { - let eventName = "event-" + event[1] + "-" + event[2] + const eventName = `event-${event[1]}-${event[2]}`; this.emit('event', { - 'name': eventName, - 'value': event[3] - }) + name: eventName, + value: event[3] + }); } this.lastEvent = now(); } - /** - * Tell the CCU that we want to receive events - */ initInterface() { - var self = this; - this.rpcClient.methodCall('init', [self.getInitUrl(), 'homey_' + self.interfaceName], function (err, res) { + this.rpcClient.methodCall('init', [this.getInitUrl(), `homey_${this.interfaceName}`], (err, res) => { if (err) { - self.logger.log('info', "Failed to connect:", self.interfaceName, err) - self.connected = false; - if (!self.retrying) { - self.retrying = true; - self.retryConnectTimer = setInterval(self.retryConnect, self.reconnectInterval, self); + this.logger.log('info', "Failed to connect:", this.interfaceName, err); + this.connected = false; + if (!this.retrying) { + this.retrying = true; + this.retryConnectTimer = setInterval(() => this.retryConnect(), this.reconnectInterval); } } else { - self.logger.log('info', "Connected to", self.interfaceName) - self.failureCount = 0; - self.connected = true; - self.wasConnected = true; - self.retrying = false; - clearInterval(self.retryConnectTimer); - self.lastEvent = now(); - self.checkConnection(); + this.logger.log('info', "Connected to", this.interfaceName); + this.failureCount = 0; + this.connected = true; + this.wasConnected = true; + this.retrying = false; + clearInterval(this.retryConnectTimer); + this.lastEvent = now(); + this.checkConnection(); } }); } checkConnection() { - var self = this; clearTimeout(this.rpcPingTimer); const pingTimeout = this._settings.pingTimeout; const elapsed = Math.round((now() - this.lastEvent) / 1000); + if (elapsed > pingTimeout) { - self.logger.log('info', 'ping timeout', this.interfaceName, elapsed); - self.initInterface(); + this.logger.log('info', 'ping timeout', this.interfaceName, elapsed); + this.initInterface(); return; } if (elapsed >= (pingTimeout / 2)) { - self.logger.log('info', "Sending ping to ", this.interfaceName) - self.rpcClient.methodCall('ping', ['homey'], (err, res) => { - - }); + this.logger.log('info', "Sending ping to ", this.interfaceName); + this.rpcClient.methodCall('ping', ['homey'], (err, res) => { }); } - this.rpcPingTimer = setTimeout(() => { - this.checkConnection(); - }, pingTimeout * 250); + this.rpcPingTimer = setTimeout(() => this.checkConnection(), pingTimeout * 250); } - /** - * Tell the CCU that we no longer want to receive events - */ unsubscribe() { - this.rpcClient.methodCall('init', [this.getInitUrl(), ''], function (err, res) { - }); + this.rpcClient.methodCall('init', [this.getInitUrl(), ''], (err, res) => { }); } listDevices() { - var self = this; - return new Promise(function (resolve, reject) { - if (!self.connected) { - resolve({ 'interfaceName': self.interfaceName, 'devices': [] }) + return new Promise((resolve, reject) => { + if (!this.connected) { + resolve({ 'interfaceName': this.interfaceName, 'devices': [] }); } else { - self.rpcClient.methodCall('listDevices', [], function (err, res) { + this.rpcClient.methodCall('listDevices', [], (err, res) => { if (err) { - reject(err) + reject(err); } else { - let devices = [] - for (var i = 0; i < res.length; i++) { - res[i].HomeyInterfaceName = self.interfaceName - devices.push(res[i]) - } - resolve({ 'interfaceName': self.interfaceName, 'devices': devices }) + const devices = res.map(device => { + device.HomeyInterfaceName = this.interfaceName; + return device; + }); + resolve({ 'interfaceName': this.interfaceName, 'devices': devices }); } }); } - }) + }); } getValue(address, key) { - var self = this; - return new Promise(function (resolve, reject) { - self.rpcClient.methodCall('getValue', [address, key], function (err, res) { + return new Promise((resolve, reject) => { + this.rpcClient.methodCall('getValue', [address, key], (err, res) => { if (err) { - reject(err) + reject(err); } else { - resolve(res) + resolve(res); } - }) - }) + }); + }); } setValue(address, key, value) { - var self = this; - return new Promise(function (resolve, reject) { - self.rpcClient.methodCall('setValue', [address, key, value], function (err, res) { + return new Promise((resolve, reject) => { + this.rpcClient.methodCall('setValue', [address, key, value], (err, res) => { if (err) { - reject(err) + reject(err); } else { - resolve(res) + resolve(res); } - }) - }) + }); + }); } } @@ -203,4 +184,4 @@ function now() { return (new Date()).getTime(); } -module.exports = InterfaceConnection; \ No newline at end of file +module.exports = InterfaceConnection; diff --git a/lib/convert.js b/lib/convert.js index e820639..6897226 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -1,57 +1,41 @@ module.exports = { - toFloat: function (value) { + toFloat(value) { return parseFloat(value); }, - toString: function (value) { + toString(value) { return value.toString(); }, - toInt: function (value) { + toInt(value) { return parseInt(value); }, - toBoolean: function (value) { - if (value === 0) { - return false - } - return true - }, - levelToOnOff: function (value) { - if (value > 0) { - return true - } - return value = false - }, - toggleBoolean: function (value) { - if (value === true) { - return false - } - return true - }, - onOffToLevel: function (value) { - if (value === true) { - return "0.99" - } else { - return "0.0" - } - }, - WhToKWh: function (value) { - return parseFloat(value) / 1000 - }, - floatToPercent: function (value) { - return Math.floor(parseFloat(value) * 100) - }, - mAToA: function (value) { - return parseFloat(value) / 1000 - }, - toTrue: function (value) { - return true - }, - tofix: function (value) { - return value.toFixed(2) - }, - faultLowbatToBoolean: function(value) { - if (value === 6) { - return true - } - return false + toBoolean(value) { + return value !== 0; + }, + levelToOnOff(value) { + return value > 0; + }, + toggleBoolean(value) { + return !value; + }, + onOffToLevel(value) { + return value ? "0.99" : "0.0"; + }, + WhToKWh(value) { + return parseFloat(value) / 1000; + }, + floatToPercent(value) { + return Math.floor(parseFloat(value) * 100); + }, + mAToA(value) { + return parseFloat(value) / 1000; + }, + toTrue() { + return true; + }, + tofix(value) { + return parseFloat(value).toFixed(2); + }, + faultLowbatToBoolean(value) { + return value === 6; } -} +}; diff --git a/lib/device.js b/lib/device.js index 4b8fe50..a3c7fc9 100644 --- a/lib/device.js +++ b/lib/device.js @@ -8,169 +8,151 @@ class Device extends Homey.Device { onInit(capabilityMap) { this.logger = this.homey.app.logger; this.capabilityMap = capabilityMap; - this.deviceAddress = this.getData().id - this.HomeyInterfaceName = this.getData().attributes.HomeyInterfaceName + this.deviceAddress = this.getData().id; + this.HomeyInterfaceName = this.getData().attributes.HomeyInterfaceName; this.bridgeSerial = this.getSetting('ccuSerial'); - if (this.bridgeSerial === null || this.bridgeSerial === "") { + if (!this.bridgeSerial) { this.bridgeSerial = this.getData().attributes.bridgeSerial; } - var self = this; this.addedEvents = []; - this.driver.getBridge({serial: this.bridgeSerial}) - .then(bridge => { - self.bridge = bridge; - self.initilizeCapabilities(); - self.registerCapabilityListeners(); - self.setSettings({ - address: self.deviceAddress, - ccuIP: self.bridge.ccuIP, - ccuSerial: self.bridge.serial, - driver: self.driver.manifest.id - }) - }) - .catch(err => { - this.error(err); + + this.driver.getBridge({ serial: this.bridgeSerial }) + .then(bridge => { + this.bridge = bridge; + this.initilizeCapabilities(); + this.registerCapabilityListeners(); + return this.setSettings({ + address: this.deviceAddress, + ccuIP: this.bridge.ccuIP, + ccuSerial: this.bridge.serial, + driver: this.driver.manifest.id }); + }) + .catch(err => { + this.error('Failed to initialize device:', err); + }); } onDeleted() { - this.addedEvents.forEach((eventName) => { + this.addedEvents.forEach(eventName => { this.bridge.removeAllListeners(eventName); - }) + }); } setValue(channel, key, value) { - var self = this; - this.bridge.setValue(this.HomeyInterfaceName, this.deviceAddress + ':' + channel, key, value).then((res) => { - }).then(() => { - return - }).catch((err) => { - self.logger.log('info', 'Set', key, 'failed for device - Value ' + value, this.deviceAddress) - throw new Error('Failed to set value', null); - }) + this.bridge.setValue(this.HomeyInterfaceName, `${this.deviceAddress}:${channel}`, key, value) + .catch(err => { + this.logger.log('info', 'Set', key, 'failed for device - Value', value, this.deviceAddress); + throw new Error('Failed to set value'); + }); } registerCapabilityListeners() { - var self = this; - Object.keys(this.capabilityMap).forEach((capabilityName) => { - if (self.capabilityMap[capabilityName].set) { + Object.keys(this.capabilityMap).forEach(capabilityName => { + if (this.capabilityMap[capabilityName].set) { this.registerCapabilityListener(capabilityName, async (value, opts) => { let setValue = value; - if (self.capabilityMap[capabilityName].set.convertMQTT && self.bridge.transport === Constants.TRANSPORT_MQTT) { - setValue = self.capabilityMap[capabilityName].set.convertMQTT(value) - } else if (self.capabilityMap[capabilityName].set.convert) { - setValue = self.capabilityMap[capabilityName].set.convert(value) + if (this.capabilityMap[capabilityName].set.convertMQTT && this.bridge.transport === Constants.TRANSPORT_MQTT) { + setValue = this.capabilityMap[capabilityName].set.convertMQTT(value); + } else if (this.capabilityMap[capabilityName].set.convert) { + setValue = this.capabilityMap[capabilityName].set.convert(value); } else { - setValue = this.convertValue(self.capabilityMap[capabilityName].set.valueType, value); + setValue = this.convertValue(this.capabilityMap[capabilityName].set.valueType, value); } - let key = self.capabilityMap[capabilityName].set.key - // console.log(self.capabilityMap[capabilityName].set.convertKey) - if (self.capabilityMap[capabilityName].set.convertKey) { - key = self.capabilityMap[capabilityName].set.convertKey(key, value) + + let key = this.capabilityMap[capabilityName].set.key; + if (this.capabilityMap[capabilityName].set.convertKey) { + key = this.capabilityMap[capabilityName].set.convertKey(key, value); } - let channel = self.capabilityMap[capabilityName].set.channel - if (self.capabilityMap[capabilityName].set.convertChannel) { - key = self.capabilityMap[capabilityName].set.convertChannel(channel, value) + + let channel = this.capabilityMap[capabilityName].set.channel; + if (this.capabilityMap[capabilityName].set.convertChannel) { + channel = this.capabilityMap[capabilityName].set.convertChannel(channel, value); } - this.setValue(channel, key, setValue) - }) + + await this.setValue(channel, key, setValue); + }); } - }) + }); } initilizeCapabilities() { - var self = this; - Object.keys(this.capabilityMap).forEach((name) => { - // Setting initial values - if (self.capabilityMap[name].channel && self.capabilityMap[name].key) { - self.getCapabilityValue(name); - self.initializeEventListener(name); + Object.keys(this.capabilityMap).forEach(name => { + if (this.capabilityMap[name].channel && this.capabilityMap[name].key) { + this.getCapabilityValue(name); + this.initializeEventListener(name); } - }) - self.initializeExtraEventListeners() + }); + this.initializeExtraEventListeners(); } getCapabilityValue(capabilityName) { - var self = this; - this.bridge.getValue(self.HomeyInterfaceName, self.deviceAddress + ':' + self.capabilityMap[capabilityName].channel, self.capabilityMap[capabilityName].key).then((value) => { - if (self.capabilityMap[capabilityName].convertMQTT && self.bridge.transport === Constants.TRANSPORT_MQTT) { - value = self.capabilityMap[capabilityName].convertMQTT(value) - } else if (self.capabilityMap[capabilityName].convert) { - value = self.capabilityMap[capabilityName].convert(value) - } else { - value = this.convertValue(self.capabilityMap[capabilityName].valueType, value); - } - self.setCapabilityValue(capabilityName, value).catch((err) => { - self.logger.log('error', 'Failed to set capability ', capabilityName, 'for device ', self.getName(), '(', self.deviceAddress, ')', err); + this.bridge.getValue(this.HomeyInterfaceName, `${this.deviceAddress}:${this.capabilityMap[capabilityName].channel}`, this.capabilityMap[capabilityName].key) + .then(value => { + value = this.convertCapabilityValue(value, capabilityName); + return this.setCapabilityValue(capabilityName, value); }) - }).catch((err) => { - }) + .catch(err => { + this.logger.log('error', `Failed to get capability ${capabilityName} for device ${this.getName()} (${this.deviceAddress})`, err); + }); } initializeEventListener(capabilityName) { - var self = this; - var eventName = 'event-' + self.deviceAddress + ':' + self.capabilityMap[capabilityName].channel + '-' + self.capabilityMap[capabilityName].key; - this.bridge.on(eventName, (value) => { - if (self.capabilityMap[capabilityName].convert) { - value = self.capabilityMap[capabilityName].convert(value) - } else { - value = this.convertValue(self.capabilityMap[capabilityName].valueType, value); - } + const eventName = `event-${this.deviceAddress}:${this.capabilityMap[capabilityName].channel}-${this.capabilityMap[capabilityName].key}`; + this.bridge.on(eventName, async value => { + value = this.convertCapabilityValue(value, capabilityName); if (value !== undefined) { - self.setCapabilityValue(capabilityName, value).catch((err) => { - self.logger.log('error', 'Failed to set capability ', capabilityName, 'for device ', self.getName(), '(', self.deviceAddress, ')', err); - }) + await this.setCapabilityValue(capabilityName, value).catch(err => { + this.logger.log('error', `Failed to set capability ${capabilityName} for device ${this.getName()} (${this.deviceAddress})`, err); + }); } }); - self.addedEvents.push(eventName); + this.addedEvents.push(eventName); } initializeExtraEventListeners() { + // Implement additional event listeners if needed + } + + convertCapabilityValue(value, capabilityName) { + const { convert, convertMQTT, valueType } = this.capabilityMap[capabilityName]; + if (convertMQTT && this.bridge.transport === Constants.TRANSPORT_MQTT) { + return convertMQTT(value); + } else if (convert) { + return convert(value); + } else { + return this.convertValue(valueType, value); + } } convertValue(valueType, value) { - if (valueType === 'string') { - value = value.toString(); - } else if (valueType === 'int') { - value = parseInt(value) - } else if (valueType === 'float') { - value = parseFloat(value) - } else if (valueType === 'boolean') { - if (value === 0) { - value = false - } else if (value === 1) { - value = true - } - } else if (valueType === 'onOffDimGet') { - if (value > 0) { - value = true - } else { - value = false - } - } else if (valueType === 'keymatic') { - value = true; - } else if (valueType === 'keymatic_swap') { - if (value === true) { - value = false - } else { - value = true - } - } else if (valueType === 'onOffDimSet') { - if (value === true) { - value = 0.99 - } else { - value = "0.0" - } - } else if (valueType === 'Wh') { - value = parseFloat(value) / 1000 - } else if (valueType === 'floatPercent') { - value = parseFloat(value) * 100 - } else if (valueType === 'mA') { - value = parseFloat(value) / 1000 + switch (valueType) { + case 'string': + return value.toString(); + case 'int': + return parseInt(value); + case 'float': + return parseFloat(value); + case 'boolean': + return value === 1; + case 'onOffDimGet': + return value > 0; + case 'keymatic': + return true; + case 'keymatic_swap': + return !value; + case 'onOffDimSet': + return value ? 0.99 : "0.0"; + case 'Wh': + return parseFloat(value) / 1000; + case 'floatPercent': + return parseFloat(value) * 100; + case 'mA': + return parseFloat(value) / 1000; + default: + return value; } - return value; } } - module.exports = Device; diff --git a/lib/driver.js b/lib/driver.js index 2a01fae..0e3673b 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -4,122 +4,114 @@ const Homey = require('homey'); class Driver extends Homey.Driver { - onInit() { - this.multiDevice = false - this.numDevices = 1 + onInit() { + this.multiDevice = false; + this.numDevices = 1; + } + + async getBridge({ serial }) { + if (!serial && Object.keys(this.homey.app.bridges).length > 0) { + return this.homey.app.bridges[Object.keys(this.homey.app.bridges)[0]]; } - async getBridge({serial}) { - let self = this; - //backwards compatibility - if (serial === undefined && Object.keys(this.homey.app.bridges).length > 0) { - return this.homey.app.bridges[Object.keys(this.homey.app.bridges)[0]] - } - - if (this.homey.app.bridges[serial]) - return this.homey.app.bridges[serial]; - - //self.homey.app.bridges = await self.homey.app.discovery.discover(); - await this.homey.app.discovery.discover(); - //backwards compatibility - if (serial === undefined && Object.keys(this.homey.app.bridges).length > 0) { - return self.homey.app.bridges[Object.keys(this.homey.app.bridges)[0]] - } - - if (this.homey.app.bridges[serial]) - return this.homey.app.bridges[serial]; - - throw new Error('Bridge not found'); + if (this.homey.app.bridges[serial]) { + return this.homey.app.bridges[serial]; } - async onPair(session) { - let currentBridge; - let self = this; - - const onListDevices = (data) => { - if (!currentBridge) - return onListDevicesBridges(data); + await this.homey.app.discovery.discover(); - return onListDevicesDevices(data); - } - - const onListDevicesBridges = async (data) => { - try { - await self.homey.app.discovery.discover() - - const result = Object.values(self.homey.app.bridges).map(bridge => { - return { - name: "CCU(" + bridge.address + ")", - data: { - serial: bridge.serial, - } - } - }); + if (!serial && Object.keys(this.homey.app.bridges).length > 0) { + return this.homey.app.bridges[Object.keys(this.homey.app.bridges)[0]]; + } - return result; + if (this.homey.app.bridges[serial]) { + return this.homey.app.bridges[serial]; + } - } catch (err) { - throw new Error('Discovery failed') - } - } - - const onListDevicesDevices = async (data) => { - if (!currentBridge) - throw new Error('Missing Bridge'); - - let devices = []; - var self = this; - - try { - let bridgeDevices = await currentBridge.listDevices() - - Object.keys(bridgeDevices).forEach((interfaceName) => { - for (var i = 0; i < bridgeDevices[interfaceName].length; i++) { - if (self.homematicTypes.includes(bridgeDevices[interfaceName][i].TYPE)) { - for (let idx = 0; idx < this.numDevices; idx++) { - let device = { - "name": this.getDeviceName(bridgeDevices[interfaceName][i].ADDRESS, idx), - "capabilities": self.capabilities, - "data": { - "id": bridgeDevices[interfaceName][i].ADDRESS, - "attributes": { - "HomeyInterfaceName": interfaceName, - "bridgeSerial": currentBridge.serial - } - } - } - if (this.multiDevice) { - device.data.attributes.Index = idx - } - devices.push(device); + throw new Error('Bridge not found'); + } + + async onPair(session) { + let currentBridge; + const self = this; + + const onListDevices = (data) => { + if (!currentBridge) { + return onListDevicesBridges(data); + } + return onListDevicesDevices(data); + }; + + const onListDevicesBridges = async (data) => { + try { + await self.homey.app.discovery.discover(); + const result = Object.values(self.homey.app.bridges).map(bridge => ({ + name: `CCU(${bridge.address})`, + data: { + serial: bridge.serial, + } + })); + + return result; + } catch (err) { + throw new Error('Discovery failed'); + } + }; + + const onListDevicesDevices = async (data) => { + if (!currentBridge) { + throw new Error('Missing Bridge'); + } + + let devices = []; + + try { + const bridgeDevices = await currentBridge.listDevices(); + Object.keys(bridgeDevices).forEach(interfaceName => { + bridgeDevices[interfaceName].forEach(device => { + if (self.homematicTypes.includes(device.TYPE)) { + for (let idx = 0; idx < this.numDevices; idx++) { + const deviceObj = { + name: this.getDeviceName(device.ADDRESS, idx), + capabilities: self.capabilities, + data: { + id: device.ADDRESS, + attributes: { + HomeyInterfaceName: interfaceName, + bridgeSerial: currentBridge.serial } } + }; + if (this.multiDevice) { + deviceObj.data.attributes.Index = idx; } - }) - } catch (err) { - throw new Error('Failed to list devices: ' + err) + devices.push(deviceObj); + } } - - return devices; - - } - - const onListBridgesSelection = async (data) => { - currentBridge = self.homey.app.bridges[data[0].data.serial]; - } - - session.setHandler('list_devices', onListDevices); - session.setHandler('list_bridges_selection', onListBridgesSelection); - - } - - getDeviceName(address, idx) { - if (this.multiDevice == true) { - return address + "-" + (idx + 1) - } else { - return address - } + }); + }); + } catch (err) { + throw new Error('Failed to list devices: ' + err); + } + + return devices; + }; + + const onListBridgesSelection = async (data) => { + currentBridge = self.homey.app.bridges[data[0].data.serial]; + }; + + session.setHandler('list_devices', onListDevices); + session.setHandler('list_bridges_selection', onListBridgesSelection); + } + + getDeviceName(address, idx) { + if (this.multiDevice) { + return `${address}-${idx + 1}`; + } else { + return address; } + } } module.exports = Driver; diff --git a/package.json b/package.json index 3f8b20f..7daac0b 100644 --- a/package.json +++ b/package.json @@ -5,18 +5,13 @@ "main": "app.js", "dependencies": { "@twendt/binrpc": "3.3.2", - "axios": "^0.21.1", - "binary": "^0.3.0", + "axios": "^1.7.2", "freeport": "1.0.5", "homematic-xmlrpc": "^1.0.2", - "mqtt": "^3.0.0", - "node-fetch": "^2.6.1", - "put": "0.0.6", - "which": "^2.0.2" + "mqtt": "^3.0.0" }, "devDependencies": { - "eslint": "^7.13.0", - "homey": "^2.14.2" + "homey": "^3.0.0" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" From 44e4a35ad84a5b60ba9c8c187c5710b74c5cc7f7 Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Fri, 31 May 2024 23:41:50 +0200 Subject: [PATCH 03/14] install --- lib/HomeMaticCCUJack.js | 148 +++++++++++++++++++------------------- lib/HomeMaticDiscovery.js | 5 +- lib/device.js | 10 +-- 3 files changed, 79 insertions(+), 84 deletions(-) diff --git a/lib/HomeMaticCCUJack.js b/lib/HomeMaticCCUJack.js index 60f083c..f1e981f 100644 --- a/lib/HomeMaticCCUJack.js +++ b/lib/HomeMaticCCUJack.js @@ -5,7 +5,7 @@ const axios = require('axios'); const mqtt = require('mqtt'); const Constants = require('./constants.js'); -const mqttStatusPrefix = 'device/status/' +const mqttStatusPrefix = 'device/status/'; class HomeMaticCCUJack extends EventEmitter { constructor(logger, homey, type, serial, ccuIP, mqttPort, user, password) { @@ -40,12 +40,13 @@ class HomeMaticCCUJack extends EventEmitter { let self = this; self.MQTTClient.subscribe(name, function(err) { if (err) { - self.logger.log('error', err); + self.logger.log('error', 'Failed to subscribe to topic', name, err); } else { - // self.logger.log('info', "Subscribed to topic", name); + self.logger.log('info', 'Subscribed to topic', name); } - }) + }); } + setupMqtt(user, password) { let self = this; let options = {}; @@ -53,76 +54,73 @@ class HomeMaticCCUJack extends EventEmitter { options.username = user; options.password = password; } - this.MQTTClient = mqtt.connect('mqtt://'+self.ccuIP+':'+self.mqttPort, options); + this.MQTTClient = mqtt.connect('mqtt://' + self.ccuIP + ':' + self.mqttPort, options); this.MQTTClient.on('connect', function() { - self.logger.log('info', 'Connected to CCU Jack broker at: ', self.ccuIP + ':'+self.mqttPort ) + self.logger.log('info', 'Connected to CCU Jack broker at:', self.ccuIP + ':' + self.mqttPort); self.connected = true; self.subscribedTopics.forEach(topic => { self.subscribeTopic(topic); - }) - }) + }); + }); this.MQTTClient.on('message', function(topic, message) { if (topic.startsWith(mqttStatusPrefix)) { let devTopic = topic.replace(mqttStatusPrefix, ''); let parts = devTopic.split('/'); - if (parts.length != 3 ) { - // console.log("Wrong message received: ", topic, devTopic, parts); - return + if (parts.length !== 3) { + self.logger.log('error', 'Received malformed message', topic, devTopic, parts); + return; } let address, channel, datapoint; [address, channel, datapoint] = parts; let msg = JSON.parse(message.toString()); let eventName = self.getEventName(address, channel, datapoint); - //self.logger.log("info", "Emitting event", eventName, "with value", msg.v) self.emit(eventName, msg.v); } }); - + this.MQTTClient.on('close', function() { if (self.connected) { - self.logger.log('info', 'MQTT disconnected') + self.logger.log('info', 'MQTT disconnected'); } self.connected = false; - }) + }); this.MQTTClient.on('offline', function() { if (self.connected) { - self.logger.log('info', 'MQTT disconnected') + self.logger.log('info', 'MQTT went offline'); } self.connected = false; - }) + }); this.MQTTClient.on('error', function(err) { - self.logger.log('error', 'MQTT error connecting:', err) + self.logger.log('error', 'MQTT connection error:', err); self.connected = false; - }) - + }); } getEventName(address, channel, datapoint) { - return "event-" + address + ":" +channel + "-" + datapoint + return "event-" + address + ":" + channel + "-" + datapoint; } on(event, callback) { let self = this; super.on(event, callback); - const prefix = "event-"; - if (event.startsWith(prefix)) { - let tmp = event.replace(prefix, ""); - let channel, datapoint; - [channel, datapoint] = tmp.split("-"); - channel = channel.replace(":", "/") - let topic = mqttStatusPrefix+channel+'/'+datapoint; - if (!self.subscribedTopics.includes(topic)) { - self.subscribedTopics.push(topic); - } - if (self.connected) { - //self.logger.log("info", "Subscribing topic", topic) - self.subscribeTopic(topic); - } - } + const prefix = "event-"; + if (event.startsWith(prefix)) { + let tmp = event.replace(prefix, ""); + let channel, datapoint; + [channel, datapoint] = tmp.split("-"); + channel = channel.replace(":", "/"); + let topic = mqttStatusPrefix + channel + '/' + datapoint; + if (!self.subscribedTopics.includes(topic)) { + self.subscribedTopics.push(topic); + } + if (self.connected) { + self.subscribeTopic(topic); + } + } } async listDevices() { @@ -132,92 +130,90 @@ class HomeMaticCCUJack extends EventEmitter { return new Promise((resolve, reject) => { self.jackClient.get('device') .then(resp => { - let deviceLinks = resp.data['~links'] - let devPromises = [] + let deviceLinks = resp.data['~links']; + let devPromises = []; deviceLinks.forEach(link => { - let p = self.jackClient.get('device/' + link['href']) - devPromises.push(p) - }) + let p = self.jackClient.get('device/' + link['href']); + devPromises.push(p); + }); Promise.all(devPromises) .then(results => { let devices = {}; - for (var i = 0; i < results.length; i++) { - let dev = results[i].data + for (let i = 0; i < results.length; i++) { + let dev = results[i].data; if (devices[dev.interfaceType] === undefined) { - devices[dev.interfaceType] = [] + devices[dev.interfaceType] = []; } devices[dev.interfaceType].push({ TYPE: dev.type, ADDRESS: dev.address - }) + }); } - resolve(devices) + resolve(devices); }) .catch(err => { - self.logger.log("error", "Failed to process devices:", err) - reject(err) - }) + self.logger.log("error", "Failed to process devices:", err); + reject(err); + }); }) .catch(err => { - console.log("error", "Failed to get devices:", err) - reject(err) - }) - }) + self.logger.log("error", "Failed to get devices:", err); + reject(err); + }); + }); } getInterfaceName(dev) { if (dev.address.startsWith('CUX')) { - return 'CUxD' + return 'CUxD'; } if (dev.type.toLowerCase().startsWith('hm-')) { - return 'BidCos-RF' + return 'BidCos-RF'; } if (dev.type.toLowerCase().startsWith('hmip-')) { - return 'HmIP-RF' + return 'HmIP-RF'; } - return '' + return ''; } - getValue(interfaceName, address, key) { + async getValue(interfaceName, address, key) { let self = this; return new Promise(function(resolve, reject) { - let valueURL = 'device/'+address.replace(':', '/') + '/' + key + '/~pv'; + let valueURL = 'device/' + address.replace(':', '/') + '/' + key + '/~pv'; self.jackClient.get(valueURL) - .then(resp =>{ - resolve(resp.data.v) + .then(resp => { + resolve(resp.data.v); }) .catch(err => { - self.logger.log("error", "Failed to get device value:", address, key, err) - reject(err) - }) - }) + self.logger.log("error", "Failed to get device value:", address, key, err); + reject(new Error('Failed to get value')); + }); + }); } - setValue(interfaceName, address, key, value) { + async setValue(interfaceName, address, key, value) { let self = this; let myValue = value; if (myValue === "1.0") { - myValue = 1 + myValue = 1; } else if (myValue === "0.0") { - myValue = 0 + myValue = 0; } return new Promise(function(resolve, reject) { - self.MQTTClient.publish('device/set/'+address.replace(':', '/')+'/'+key, JSON.stringify(myValue), function(err) { + self.MQTTClient.publish('device/set/' + address.replace(':', '/') + '/' + key, JSON.stringify(myValue), function(err) { if (err) { - self.logger.log('error', 'Failed to publish message: ', err); - reject(err); + self.logger.log('error', 'Failed to publish message:', err); + reject(new Error('Failed to set value')); } else { - // console.log("Message published") + resolve(true); } }); - resolve(true) - }) + }); } - } -module.exports = HomeMaticCCUJack; \ No newline at end of file +module.exports = HomeMaticCCUJack; diff --git a/lib/HomeMaticDiscovery.js b/lib/HomeMaticDiscovery.js index f006ac6..769fff4 100644 --- a/lib/HomeMaticDiscovery.js +++ b/lib/HomeMaticDiscovery.js @@ -1,7 +1,7 @@ 'use strict'; const dgram = require('dgram'); -const Constants = require('./constants') +const Constants = require('./constants'); const DISCOVER_MESSAGE = Buffer.from([0x02, 0x8F, 0x91, 0xC0, 0x01, 'e', 'Q', '3', 0x2D, 0x2A, 0x00, 0x2A, 0x00, 0x49]); const DISCOVER_TIMEOUT = 2000; @@ -17,14 +17,13 @@ module.exports = class HomeMaticDiscovery { } _onClientMessage(message, remote) { - var self = this; const { address, } = remote; const headerStart = 0; const headerEnd = 5; - const header = message.slice(headerStart, headerEnd); + // const header = message.slice(headerStart, headerEnd); // Commented out as it is not used const typeStart = headerEnd; const typeEnd = message.indexOf(0x00); diff --git a/lib/device.js b/lib/device.js index a3c7fc9..54320fe 100644 --- a/lib/device.js +++ b/lib/device.js @@ -42,8 +42,8 @@ class Device extends Homey.Device { setValue(channel, key, value) { this.bridge.setValue(this.HomeyInterfaceName, `${this.deviceAddress}:${channel}`, key, value) .catch(err => { - this.logger.log('info', 'Set', key, 'failed for device - Value', value, this.deviceAddress); - throw new Error('Failed to set value'); + this.logger.log('info', 'Set', key, 'failed for device - Value', value, this.deviceAddress); + throw new Error('Failed to set value'); }); } @@ -70,7 +70,7 @@ class Device extends Homey.Device { channel = this.capabilityMap[capabilityName].set.convertChannel(channel, value); } - await this.setValue(channel, key, setValue); + this.setValue(channel, key, setValue); }); } }); @@ -89,11 +89,11 @@ class Device extends Homey.Device { getCapabilityValue(capabilityName) { this.bridge.getValue(this.HomeyInterfaceName, `${this.deviceAddress}:${this.capabilityMap[capabilityName].channel}`, this.capabilityMap[capabilityName].key) .then(value => { - value = this.convertCapabilityValue(value, capabilityName); + value = this.convertCapabilityValue(value, capabilityName); return this.setCapabilityValue(capabilityName, value); }) .catch(err => { - this.logger.log('error', `Failed to get capability ${capabilityName} for device ${this.getName()} (${this.deviceAddress})`, err); + this.logger.log('error', `Failed to get capability ${capabilityName} for device ${this.getName()} (${this.deviceAddress})`, err); }); } From 2bf6b1a054922d4d203e90bd105c944b1496b836 Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Sat, 1 Jun 2024 09:26:30 +0200 Subject: [PATCH 04/14] Bug fixes and code optimizations --- app.js | 108 +++++++++++++++++------------------ drivers/HmIP-SWO-B/device.js | 2 +- lib/HomeMaticDiscovery.js | 82 ++++++++++++++++---------- 3 files changed, 106 insertions(+), 86 deletions(-) diff --git a/app.js b/app.js index 538db42..b6b254b 100644 --- a/app.js +++ b/app.js @@ -6,27 +6,31 @@ const HomeMaticCCUMQTT = require('./lib/HomeMaticCCUMQTT'); const HomeMaticCCURPC = require('./lib/HomeMaticCCURPC'); const HomeMaticCCUJack = require('./lib/HomeMaticCCUJack'); const Constants = require('./lib/constants'); -const Logger = require('./lib/logger') +const Logger = require('./lib/logger'); -const connTypeRPC = 'use_rpc' -const connTypeMQTT = 'use_mqtt' -const connTypeCCUJack = 'use_ccu_jack' +const connTypeRPC = 'use_rpc'; +const connTypeMQTT = 'use_mqtt'; +const connTypeCCUJack = 'use_ccu_jack'; class Homematic extends Homey.App { async onInit() { this.logger = new Logger(this.homey); this.logger.log('info', 'Started homematic...'); - var self = this; - let address = await this.homey.cloud.getLocalAddress() - self.homeyIP = address.split(':')[0] - self.settings = self.getSettings(); - self.discovery = new HomeMaticDiscovery(this.logger, this.homey); - self.bridges = {}; - if (this.homey.app.settings.use_stored_bridges) { - self.initializeStoredBridges(); + + const address = await this.homey.cloud.getLocalAddress(); + this.homeyIP = address.split(':')[0]; + this.settings = this.getSettings(); + this.discovery = new HomeMaticDiscovery(this.logger, this.homey); + this.bridges = {}; + + if (this.settings.use_stored_bridges) { + this.logger.log('info', 'Initializing stored bridges...'); + this.initializeStoredBridges(); } else { - await self.discovery.discover() + this.logger.log('info', 'Starting discovery process...'); + await this.discovery.discover(); + this.logger.log('info', 'Discovery process finished.'); } } @@ -38,84 +42,81 @@ class Homematic extends Homey.App { "ccu_jack_user": this.homey.settings.get('ccu_jack_user'), "ccu_jack_password": this.homey.settings.get('ccu_jack_password'), "use_stored_bridges": this.homey.settings.get('use_stored_bridges'), - } + }; } getStoredBridges() { - var self = this; - var bridges = {}; + const bridges = {}; this.homey.settings.getKeys().forEach((key) => { if (key.startsWith(Constants.SETTINGS_PREFIX_BRIDGE)) { - let bridge = this.homey.settings.get(key); - bridges[bridge.serial] = bridge + const bridge = this.homey.settings.get(key); + bridges[bridge.serial] = bridge; } - }) - - return bridges + }); + return bridges; } initializeStoredBridges() { - var self = this; - var bridges = this.getStoredBridges(); + const bridges = this.getStoredBridges(); Object.keys(bridges).forEach((serial) => { - let bridge = bridges[serial]; - self.logger.log('info', "Initializing stored ccu:", "Type", bridge.type, "Serial", bridge.serial, "IP", bridge.address); - self.initializeBridge(bridge) + const bridge = bridges[serial]; + this.logger.log('info', "Initializing stored CCU:", "Type", bridge.type, "Serial", bridge.serial, "IP", bridge.address); + this.initializeBridge(bridge); }); } getConnectionType() { - if (this.homey.app.settings.connection_type) { - return this.homey.app.settings.connection_type + if (this.settings.connection_type) { + return this.settings.connection_type; } - if (this.homey.app.settings.use_mqtt === true) { - return connTypeMQTT + if (this.settings.use_mqtt) { + return connTypeMQTT; } - return connTypeRPC + return connTypeRPC; } initializeBridge(bridge) { - let self = this; - let connType = self.getConnectionType() - self.logger.log('info', 'Connection type:', connType) + const connType = this.getConnectionType(); + this.logger.log('info', 'Connection type:', connType); switch (connType) { case connTypeRPC: - self.logger.log('info', "Initializing RPC CCU"); - self.bridges[bridge.serial] = new HomeMaticCCURPC(self.logger, self.homey, bridge.type, bridge.serial, bridge.address); + this.logger.log('info', "Initializing RPC CCU"); + this.bridges[bridge.serial] = new HomeMaticCCURPC(this.logger, this.homey, bridge.type, bridge.serial, bridge.address); break; case connTypeMQTT: - self.logger.log('info', "Initializing MQTT CCU "); - self.bridges[bridge.serial] = new HomeMaticCCUMQTT(self.logger, self.homey, bridge.type, bridge.serial, bridge.address); + this.logger.log('info', "Initializing MQTT CCU"); + this.bridges[bridge.serial] = new HomeMaticCCUMQTT(this.logger, this.homey, bridge.type, bridge.serial, bridge.address); break; case connTypeCCUJack: - self.logger.log('info', "Initializing CCU Jack"); - self.bridges[bridge.serial] = new HomeMaticCCUJack( - self.logger, - self.homey, + this.logger.log('info', "Initializing CCU Jack"); + this.bridges[bridge.serial] = new HomeMaticCCUJack( + this.logger, + this.homey, bridge.type, bridge.serial, bridge.address, - this.homey.app.settings.ccu_jack_mqtt_port, - this.homey.app.settings.ccu_jack_user, - this.homey.app.settings.ccu_jack_password, + this.settings.ccu_jack_mqtt_port, + this.settings.ccu_jack_user, + this.settings.ccu_jack_password, ); break; } - - return self.bridges[bridge.serial] + return this.bridges[bridge.serial]; } setBridgeAddress(serial, address) { - var self = this; - self.bridges[serial].address = address; + if (this.bridges[serial]) { + this.bridges[serial].address = address; + } else { + this.logger.log('error', `No bridge found for serial: ${serial}`); + } } deleteStoredBridges() { - var self = this; - var bridges = this.getStoredBridges() + const bridges = this.getStoredBridges(); Object.keys(bridges).forEach((serial) => { - this.homey.settings.unset(Constants.SETTINGS_PREFIX_BRIDGE + serial) - }) + this.homey.settings.unset(Constants.SETTINGS_PREFIX_BRIDGE + serial); + }); } getLogLines() { @@ -124,5 +125,4 @@ class Homematic extends Homey.App { } - module.exports = Homematic; diff --git a/drivers/HmIP-SWO-B/device.js b/drivers/HmIP-SWO-B/device.js index b0d8a51..7b3a67a 100644 --- a/drivers/HmIP-SWO-B/device.js +++ b/drivers/HmIP-SWO-B/device.js @@ -14,7 +14,7 @@ const capabilityMap = { "key": "HUMIDITY", "convert": Convert.toFloat }, - " measure_wind_strength": { + "measure_wind_strength": { "channel": 1, "key": "WIND_SPEED", "convert": Convert.toFloat diff --git a/lib/HomeMaticDiscovery.js b/lib/HomeMaticDiscovery.js index 769fff4..7144682 100644 --- a/lib/HomeMaticDiscovery.js +++ b/lib/HomeMaticDiscovery.js @@ -4,7 +4,7 @@ const dgram = require('dgram'); const Constants = require('./constants'); const DISCOVER_MESSAGE = Buffer.from([0x02, 0x8F, 0x91, 0xC0, 0x01, 'e', 'Q', '3', 0x2D, 0x2A, 0x00, 0x2A, 0x00, 0x49]); -const DISCOVER_TIMEOUT = 2000; +const DISCOVER_TIMEOUT = 5000; // Increased timeout for network delays const CCU_PORT = 43439; const DGRAM_PORT = 48724; @@ -17,30 +17,34 @@ module.exports = class HomeMaticDiscovery { } _onClientMessage(message, remote) { - const { - address, - } = remote; + const { address } = remote; + this.logger.log('info', `Received message from ${address}`); + this.logger.log('info', `Raw message: ${message.toString('hex')}`); - const headerStart = 0; - const headerEnd = 5; - // const header = message.slice(headerStart, headerEnd); // Commented out as it is not used + try { + const headerStart = 0; + const headerEnd = 5; + const typeStart = headerEnd; + const typeEnd = message.indexOf(0x00, typeStart); + const type = message.slice(typeStart, typeEnd).toString(); - const typeStart = headerEnd; - const typeEnd = message.indexOf(0x00); - const type = message.slice(typeStart, typeEnd).toString(); + const serialStart = typeEnd + 1; + const serialEnd = message.indexOf(0x00, serialStart); + const serial = message.slice(serialStart, serialEnd).toString(); - const serialStart = typeEnd + 1; - const serialEnd = serialStart + message.slice(serialStart).indexOf(0x00); - const serial = message.slice(serialStart, serialEnd).toString(); + this.logger.log('info', `Discovered device - Type: ${type}, Serial: ${serial}, Address: ${address}`); - //if (this.devices[serial]) { - if (this.homey.app.bridges[serial]) { - //this.devices[serial].address = address; - this.homey.app.setBridgeAddress(serial, address) - } else { - this.homey.app.initializeBridge({serial: serial, type: type, address: address}) + if (this.homey.app.bridges[serial]) { + this.logger.log('info', `Updating existing bridge: ${serial}`); + this.homey.app.setBridgeAddress(serial, address); + } else { + this.logger.log('info', `Initializing new bridge: ${serial}`); + this.homey.app.initializeBridge({ serial: serial, type: type, address: address }); + } + this.homey.settings.set(Constants.SETTINGS_PREFIX_BRIDGE + serial, { serial: serial, type: type, address: address }); + } catch (err) { + this.logger.log('error', `Error processing message from ${address}:`, err); } - this.homey.settings.set(Constants.SETTINGS_PREFIX_BRIDGE + serial, {serial: serial, type: type, address: address}) } async getClient() { @@ -48,24 +52,40 @@ module.exports = class HomeMaticDiscovery { this._client = new Promise((resolve, reject) => { const client = dgram.createSocket('udp4'); client.on('message', this._onClientMessage.bind(this)); - client.bind(48724, err => { - if (err) return reject(err); + client.on('error', (err) => { + this.logger.log('error', 'UDP client error:', err); + }); + client.on('listening', () => { + const address = client.address(); + this.logger.log('info', `UDP client listening on ${address.address}:${address.port}`); + }); + client.bind(DGRAM_PORT, (err) => { + if (err) { + this.logger.log('error', 'Failed to bind UDP client:', err); + return reject(err); + } client.setBroadcast(true); resolve(client); }); }); } - return this._client; } async discover({ timeout = DISCOVER_TIMEOUT } = {}) { - const client = await this.getClient(); - client.send(DISCOVER_MESSAGE, 0, DISCOVER_MESSAGE.length, CCU_PORT, '255.255.255.255'); - await new Promise(resolve => { - setTimeout(resolve, timeout); - }); - //return this.devices; + try { + const client = await this.getClient(); + client.send(DISCOVER_MESSAGE, 0, DISCOVER_MESSAGE.length, CCU_PORT, '255.255.255.255', (err) => { + if (err) { + this.logger.log('error', 'Failed to send discovery message:', err); + } else { + this.logger.log('info', 'Discovery message sent'); + } + }); + await new Promise(resolve => setTimeout(resolve, timeout)); + this.logger.log('info', 'Discovery process completed'); + } catch (err) { + this.logger.log('error', 'Discovery failed:', err); + } } - -} \ No newline at end of file +} From b69c6694a22504ac2ad889fc8691e33953ba2bbc Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Sat, 1 Jun 2024 13:07:37 +0200 Subject: [PATCH 05/14] Fix discovery process on homey restart --- app.js | 61 ++++++++++++++++++++++++++------------- lib/HomeMaticDiscovery.js | 10 +++++-- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/app.js b/app.js index b6b254b..e939728 100644 --- a/app.js +++ b/app.js @@ -13,24 +13,28 @@ const connTypeMQTT = 'use_mqtt'; const connTypeCCUJack = 'use_ccu_jack'; class Homematic extends Homey.App { - + async onInit() { this.logger = new Logger(this.homey); this.logger.log('info', 'Started homematic...'); - - const address = await this.homey.cloud.getLocalAddress(); - this.homeyIP = address.split(':')[0]; - this.settings = this.getSettings(); - this.discovery = new HomeMaticDiscovery(this.logger, this.homey); - this.bridges = {}; - - if (this.settings.use_stored_bridges) { - this.logger.log('info', 'Initializing stored bridges...'); - this.initializeStoredBridges(); - } else { - this.logger.log('info', 'Starting discovery process...'); - await this.discovery.discover(); - this.logger.log('info', 'Discovery process finished.'); + this.discoveryInProgress = false; // Flag to prevent multiple discovery processes + + try { + const address = await this.homey.cloud.getLocalAddress(); + this.homeyIP = address.split(':')[0]; + this.settings = this.getSettings(); + this.discovery = new HomeMaticDiscovery(this.logger, this.homey); + this.bridges = {}; + + const storedBridges = this.getStoredBridges(); + if (Object.keys(storedBridges).length > 0) { + this.logger.log('info', 'Initializing stored bridges...'); + this.initializeStoredBridges(storedBridges); + } else { + this.startDiscoveryProcess(); + } + } catch (err) { + this.logger.log('error', 'Initialization failed:', err); } } @@ -49,17 +53,16 @@ class Homematic extends Homey.App { const bridges = {}; this.homey.settings.getKeys().forEach((key) => { if (key.startsWith(Constants.SETTINGS_PREFIX_BRIDGE)) { - const bridge = this.homey.settings.get(key); + let bridge = this.homey.settings.get(key); bridges[bridge.serial] = bridge; } }); return bridges; } - initializeStoredBridges() { - const bridges = this.getStoredBridges(); + initializeStoredBridges(bridges) { Object.keys(bridges).forEach((serial) => { - const bridge = bridges[serial]; + let bridge = bridges[serial]; this.logger.log('info', "Initializing stored CCU:", "Type", bridge.type, "Serial", bridge.serial, "IP", bridge.address); this.initializeBridge(bridge); }); @@ -84,7 +87,7 @@ class Homematic extends Homey.App { this.bridges[bridge.serial] = new HomeMaticCCURPC(this.logger, this.homey, bridge.type, bridge.serial, bridge.address); break; case connTypeMQTT: - this.logger.log('info', "Initializing MQTT CCU"); + this.logger.log('info', "Initializing MQTT CCU "); this.bridges[bridge.serial] = new HomeMaticCCUMQTT(this.logger, this.homey, bridge.type, bridge.serial, bridge.address); break; case connTypeCCUJack: @@ -123,6 +126,24 @@ class Homematic extends Homey.App { return this.logger.getLogLines(); } + async startDiscoveryProcess() { + if (this.discoveryInProgress) { + this.logger.log('info', 'Discovery process already in progress...'); + return; + } + + this.discoveryInProgress = true; + this.logger.log('info', 'Starting discovery process...'); + + try { + await this.discovery.discover(); + } catch (err) { + this.logger.log('error', 'Discovery process failed:', err); + } finally { + this.discoveryInProgress = false; + this.logger.log('info', 'Discovery process finished.'); + } + } } module.exports = Homematic; diff --git a/lib/HomeMaticDiscovery.js b/lib/HomeMaticDiscovery.js index 7144682..1747203 100644 --- a/lib/HomeMaticDiscovery.js +++ b/lib/HomeMaticDiscovery.js @@ -4,7 +4,7 @@ const dgram = require('dgram'); const Constants = require('./constants'); const DISCOVER_MESSAGE = Buffer.from([0x02, 0x8F, 0x91, 0xC0, 0x01, 'e', 'Q', '3', 0x2D, 0x2A, 0x00, 0x2A, 0x00, 0x49]); -const DISCOVER_TIMEOUT = 5000; // Increased timeout for network delays +const DISCOVER_TIMEOUT = 5000; // Timeout for network delays const CCU_PORT = 43439; const DGRAM_PORT = 48724; @@ -17,8 +17,8 @@ module.exports = class HomeMaticDiscovery { } _onClientMessage(message, remote) { - const { address } = remote; - this.logger.log('info', `Received message from ${address}`); + const { address, port } = remote; + this.logger.log('info', `Received message from ${address}:${port}`); this.logger.log('info', `Raw message: ${message.toString('hex')}`); try { @@ -26,10 +26,14 @@ module.exports = class HomeMaticDiscovery { const headerEnd = 5; const typeStart = headerEnd; const typeEnd = message.indexOf(0x00, typeStart); + if (typeEnd === -1) throw new Error('Invalid message format: missing type'); + const type = message.slice(typeStart, typeEnd).toString(); const serialStart = typeEnd + 1; const serialEnd = message.indexOf(0x00, serialStart); + if (serialEnd === -1) throw new Error('Invalid message format: missing serial'); + const serial = message.slice(serialStart, serialEnd).toString(); this.logger.log('info', `Discovered device - Type: ${type}, Serial: ${serial}, Address: ${address}`); From 133bd4fdba3dd4332ce0f06dc0bac3e39865b30f Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Sat, 1 Jun 2024 22:54:34 +0200 Subject: [PATCH 06/14] refactor: Simplify discovery process initialization --- app.js | 28 ++++------------------------ lib/HomeMaticDiscovery.js | 12 ++++++++++++ 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/app.js b/app.js index e939728..d0ae5bb 100644 --- a/app.js +++ b/app.js @@ -13,25 +13,24 @@ const connTypeMQTT = 'use_mqtt'; const connTypeCCUJack = 'use_ccu_jack'; class Homematic extends Homey.App { - + async onInit() { this.logger = new Logger(this.homey); this.logger.log('info', 'Started homematic...'); - this.discoveryInProgress = false; // Flag to prevent multiple discovery processes - + try { const address = await this.homey.cloud.getLocalAddress(); this.homeyIP = address.split(':')[0]; this.settings = this.getSettings(); this.discovery = new HomeMaticDiscovery(this.logger, this.homey); this.bridges = {}; - + const storedBridges = this.getStoredBridges(); if (Object.keys(storedBridges).length > 0) { this.logger.log('info', 'Initializing stored bridges...'); this.initializeStoredBridges(storedBridges); } else { - this.startDiscoveryProcess(); + await this.discovery.discover(); } } catch (err) { this.logger.log('error', 'Initialization failed:', err); @@ -125,25 +124,6 @@ class Homematic extends Homey.App { getLogLines() { return this.logger.getLogLines(); } - - async startDiscoveryProcess() { - if (this.discoveryInProgress) { - this.logger.log('info', 'Discovery process already in progress...'); - return; - } - - this.discoveryInProgress = true; - this.logger.log('info', 'Starting discovery process...'); - - try { - await this.discovery.discover(); - } catch (err) { - this.logger.log('error', 'Discovery process failed:', err); - } finally { - this.discoveryInProgress = false; - this.logger.log('info', 'Discovery process finished.'); - } - } } module.exports = Homematic; diff --git a/lib/HomeMaticDiscovery.js b/lib/HomeMaticDiscovery.js index 1747203..8a85eb7 100644 --- a/lib/HomeMaticDiscovery.js +++ b/lib/HomeMaticDiscovery.js @@ -14,6 +14,7 @@ module.exports = class HomeMaticDiscovery { this.logger = logger; this.homey = homey; this.devices = {}; + this.discoveryInProgress = false; // Flag to prevent multiple discovery processes } _onClientMessage(message, remote) { @@ -77,6 +78,14 @@ module.exports = class HomeMaticDiscovery { } async discover({ timeout = DISCOVER_TIMEOUT } = {}) { + if (this.discoveryInProgress) { + this.logger.log('info', 'Discovery process already in progress...'); + return; + } + + this.discoveryInProgress = true; + this.logger.log('info', 'Starting discovery process...'); + try { const client = await this.getClient(); client.send(DISCOVER_MESSAGE, 0, DISCOVER_MESSAGE.length, CCU_PORT, '255.255.255.255', (err) => { @@ -90,6 +99,9 @@ module.exports = class HomeMaticDiscovery { this.logger.log('info', 'Discovery process completed'); } catch (err) { this.logger.log('error', 'Discovery failed:', err); + } finally { + this.discoveryInProgress = false; + this.logger.log('info', 'Discovery process finished.'); } } } From 0cc94633d001ca69a47ed44a0e4ed962680be5bd Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Sun, 2 Jun 2024 08:59:36 +0200 Subject: [PATCH 07/14] fix: prevent multiple concurrent communication attempts --- lib/HomeMaticCCUJack.js | 231 +++++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 109 deletions(-) diff --git a/lib/HomeMaticCCUJack.js b/lib/HomeMaticCCUJack.js index f1e981f..3183295 100644 --- a/lib/HomeMaticCCUJack.js +++ b/lib/HomeMaticCCUJack.js @@ -1,4 +1,4 @@ -/*global module:true, require:false*/ +'use strict'; const EventEmitter = require('events'); const axios = require('axios'); @@ -34,68 +34,96 @@ class HomeMaticCCUJack extends EventEmitter { }; } this.jackClient = axios.create(clientOptions); + + // Initialize request queue + this.requestQueue = []; + this.isProcessingQueue = false; } - subscribeTopic(name) { - let self = this; - self.MQTTClient.subscribe(name, function(err) { - if (err) { - self.logger.log('error', 'Failed to subscribe to topic', name, err); - } else { - self.logger.log('info', 'Subscribed to topic', name); + // Method to add request to the queue + enqueueRequest(request) { + this.requestQueue.push(request); + this.processQueue(); + } + + // Method to process the request queue + async processQueue() { + if (this.isProcessingQueue || this.requestQueue.length === 0) { + return; + } + + this.isProcessingQueue = true; + while (this.requestQueue.length > 0) { + const request = this.requestQueue.shift(); + try { + await request(); + } catch (error) { + this.logger.log('error', 'Failed to process request:', error); } - }); + } + this.isProcessingQueue = false; + } + + subscribeTopic(name) { + this.enqueueRequest(() => new Promise((resolve, reject) => { + this.MQTTClient.subscribe(name, (err) => { + if (err) { + this.logger.log('error', 'Failed to subscribe to topic', name, err); + return reject(err); + } + this.logger.log('info', 'Subscribed to topic', name); + resolve(); + }); + })); } setupMqtt(user, password) { - let self = this; let options = {}; if (user && password) { options.username = user; options.password = password; } - this.MQTTClient = mqtt.connect('mqtt://' + self.ccuIP + ':' + self.mqttPort, options); - this.MQTTClient.on('connect', function() { - self.logger.log('info', 'Connected to CCU Jack broker at:', self.ccuIP + ':' + self.mqttPort); - self.connected = true; - self.subscribedTopics.forEach(topic => { - self.subscribeTopic(topic); + this.MQTTClient = mqtt.connect('mqtt://' + this.ccuIP + ':' + this.mqttPort, options); + this.MQTTClient.on('connect', () => { + this.logger.log('info', 'Connected to CCU Jack broker at:', this.ccuIP + ':' + this.mqttPort); + this.connected = true; + this.subscribedTopics.forEach(topic => { + this.subscribeTopic(topic); }); }); - this.MQTTClient.on('message', function(topic, message) { + this.MQTTClient.on('message', (topic, message) => { if (topic.startsWith(mqttStatusPrefix)) { let devTopic = topic.replace(mqttStatusPrefix, ''); let parts = devTopic.split('/'); if (parts.length !== 3) { - self.logger.log('error', 'Received malformed message', topic, devTopic, parts); + this.logger.log('error', 'Received malformed message', topic, devTopic, parts); return; } - let address, channel, datapoint; - [address, channel, datapoint] = parts; + let [address, channel, datapoint] = parts; let msg = JSON.parse(message.toString()); - let eventName = self.getEventName(address, channel, datapoint); - self.emit(eventName, msg.v); + let eventName = this.getEventName(address, channel, datapoint); + this.emit(eventName, msg.v); } }); - this.MQTTClient.on('close', function() { - if (self.connected) { - self.logger.log('info', 'MQTT disconnected'); + this.MQTTClient.on('close', () => { + if (this.connected) { + this.logger.log('info', 'MQTT disconnected'); } - self.connected = false; + this.connected = false; }); - this.MQTTClient.on('offline', function() { - if (self.connected) { - self.logger.log('info', 'MQTT went offline'); + this.MQTTClient.on('offline', () => { + if (this.connected) { + this.logger.log('info', 'MQTT went offline'); } - self.connected = false; + this.connected = false; }); - this.MQTTClient.on('error', function(err) { - self.logger.log('error', 'MQTT connection error:', err); - self.connected = false; + this.MQTTClient.on('error', (err) => { + this.logger.log('error', 'MQTT connection error:', err); + this.connected = false; }); } @@ -104,113 +132,98 @@ class HomeMaticCCUJack extends EventEmitter { } on(event, callback) { - let self = this; super.on(event, callback); const prefix = "event-"; if (event.startsWith(prefix)) { let tmp = event.replace(prefix, ""); - let channel, datapoint; - [channel, datapoint] = tmp.split("-"); + let [channel, datapoint] = tmp.split("-"); channel = channel.replace(":", "/"); let topic = mqttStatusPrefix + channel + '/' + datapoint; - if (!self.subscribedTopics.includes(topic)) { - self.subscribedTopics.push(topic); + if (!this.subscribedTopics.includes(topic)) { + this.subscribedTopics.push(topic); } - if (self.connected) { - self.subscribeTopic(topic); + if (this.connected) { + this.subscribeTopic(topic); } } } async listDevices() { - let self = this; - let allDevices = []; - return new Promise((resolve, reject) => { - self.jackClient.get('device') - .then(resp => { - let deviceLinks = resp.data['~links']; - let devPromises = []; - deviceLinks.forEach(link => { - let p = self.jackClient.get('device/' + link['href']); - devPromises.push(p); - }); + this.enqueueRequest(() => { + return this.jackClient.get('device') + .then(resp => { + let deviceLinks = resp.data['~links']; + let devPromises = []; + deviceLinks.forEach(link => { + let p = this.jackClient.get('device/' + link['href']); + devPromises.push(p); + }); - Promise.all(devPromises) - .then(results => { - let devices = {}; - for (let i = 0; i < results.length; i++) { - let dev = results[i].data; - if (devices[dev.interfaceType] === undefined) { - devices[dev.interfaceType] = []; - } - devices[dev.interfaceType].push({ - TYPE: dev.type, - ADDRESS: dev.address + return Promise.all(devPromises) + .then(results => { + let devices = {}; + results.forEach(dev => { + let device = dev.data; + if (!devices[device.interfaceType]) { + devices[device.interfaceType] = []; + } + devices[device.interfaceType].push({ + TYPE: device.type, + ADDRESS: device.address + }); }); - } - resolve(devices); - }) - .catch(err => { - self.logger.log("error", "Failed to process devices:", err); - reject(err); - }); - }) - .catch(err => { - self.logger.log("error", "Failed to get devices:", err); - reject(err); - }); + resolve(devices); + }) + .catch(err => { + this.logger.log("error", "Failed to process devices:", err); + reject(err); + }); + }) + .catch(err => { + this.logger.log("error", "Failed to get devices:", err); + reject(err); + }); + }); }); } - getInterfaceName(dev) { - if (dev.address.startsWith('CUX')) { - return 'CUxD'; - } - - if (dev.type.toLowerCase().startsWith('hm-')) { - return 'BidCos-RF'; - } - - if (dev.type.toLowerCase().startsWith('hmip-')) { - return 'HmIP-RF'; - } - - return ''; - } - async getValue(interfaceName, address, key) { - let self = this; - return new Promise(function(resolve, reject) { + return new Promise((resolve, reject) => { let valueURL = 'device/' + address.replace(':', '/') + '/' + key + '/~pv'; - self.jackClient.get(valueURL) - .then(resp => { - resolve(resp.data.v); - }) - .catch(err => { - self.logger.log("error", "Failed to get device value:", address, key, err); - reject(new Error('Failed to get value')); - }); + this.enqueueRequest(() => { + return this.jackClient.get(valueURL) + .then(resp => { + resolve(resp.data.v); + }) + .catch(err => { + this.logger.log("error", "Failed to get device value:", address, key, err); + reject(new Error('Failed to get value')); + }); + }); }); } async setValue(interfaceName, address, key, value) { - let self = this; let myValue = value; if (myValue === "1.0") { myValue = 1; } else if (myValue === "0.0") { myValue = 0; } - return new Promise(function(resolve, reject) { - self.MQTTClient.publish('device/set/' + address.replace(':', '/') + '/' + key, JSON.stringify(myValue), function(err) { - if (err) { - self.logger.log('error', 'Failed to publish message:', err); - reject(new Error('Failed to set value')); - } else { - resolve(true); - } + return new Promise((resolve, reject) => { + this.enqueueRequest(() => { + return new Promise((resolveInner, rejectInner) => { + this.MQTTClient.publish('device/set/' + address.replace(':', '/') + '/' + key, JSON.stringify(myValue), (err) => { + if (err) { + this.logger.log('error', 'Failed to publish message:', err); + rejectInner(new Error('Failed to set value')); + } else { + resolveInner(true); + } + }); + }).then(resolve).catch(reject); }); }); } From 032447a0918287eb897dbb8db60a6dd3b4e78ca0 Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Sun, 2 Jun 2024 09:54:33 +0200 Subject: [PATCH 08/14] increase robustness of network connection --- app.js | 57 +++++++++++++++++++++++------------------ lib/HomeMaticCCUJack.js | 10 +++++--- lib/connection.js | 3 ++- 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/app.js b/app.js index d0ae5bb..a971f9a 100644 --- a/app.js +++ b/app.js @@ -78,32 +78,39 @@ class Homematic extends Homey.App { } initializeBridge(bridge) { - const connType = this.getConnectionType(); - this.logger.log('info', 'Connection type:', connType); - switch (connType) { - case connTypeRPC: - this.logger.log('info', "Initializing RPC CCU"); - this.bridges[bridge.serial] = new HomeMaticCCURPC(this.logger, this.homey, bridge.type, bridge.serial, bridge.address); - break; - case connTypeMQTT: - this.logger.log('info', "Initializing MQTT CCU "); - this.bridges[bridge.serial] = new HomeMaticCCUMQTT(this.logger, this.homey, bridge.type, bridge.serial, bridge.address); - break; - case connTypeCCUJack: - this.logger.log('info', "Initializing CCU Jack"); - this.bridges[bridge.serial] = new HomeMaticCCUJack( - this.logger, - this.homey, - bridge.type, - bridge.serial, - bridge.address, - this.settings.ccu_jack_mqtt_port, - this.settings.ccu_jack_user, - this.settings.ccu_jack_password, - ); - break; + try { + const connType = this.getConnectionType(); + this.logger.log('info', 'Connection type:', connType); + switch (connType) { + case connTypeRPC: + this.logger.log('info', "Initializing RPC CCU"); + this.bridges[bridge.serial] = new HomeMaticCCURPC(this.logger, this.homey, bridge.type, bridge.serial, bridge.address); + break; + case connTypeMQTT: + this.logger.log('info', "Initializing MQTT CCU "); + this.bridges[bridge.serial] = new HomeMaticCCUMQTT(this.logger, this.homey, bridge.type, bridge.serial, bridge.address); + break; + case connTypeCCUJack: + this.logger.log('info', "Initializing CCU Jack"); + this.bridges[bridge.serial] = new HomeMaticCCUJack( + this.logger, + this.homey, + bridge.type, + bridge.serial, + bridge.address, + this.settings.ccu_jack_mqtt_port, + this.settings.ccu_jack_user, + this.settings.ccu_jack_password, + ); + break; + default: + throw new Error(`Unsupported connection type: ${connType}`); + } + return this.bridges[bridge.serial]; + } catch (err) { + this.logger.log('error', `Failed to initialize bridge ${bridge.serial}:`, err); } - return this.bridges[bridge.serial]; + } setBridgeAddress(serial, address) { diff --git a/lib/HomeMaticCCUJack.js b/lib/HomeMaticCCUJack.js index 3183295..4e4493a 100644 --- a/lib/HomeMaticCCUJack.js +++ b/lib/HomeMaticCCUJack.js @@ -21,6 +21,7 @@ class HomeMaticCCUJack extends EventEmitter { this.subscribedTopics = []; this.connected = false; this.setupMqtt(user, password); + this.mqttReconnectInterval = 5000; // Retry every 5 seconds let clientOptions = { baseURL: 'http://' + ccuIP + ':2121/', @@ -106,24 +107,27 @@ class HomeMaticCCUJack extends EventEmitter { this.emit(eventName, msg.v); } }); - + this.MQTTClient.on('close', () => { if (this.connected) { this.logger.log('info', 'MQTT disconnected'); } this.connected = false; + setTimeout(() => this.setupMqtt(user, password), this.mqttReconnectInterval); }); - + this.MQTTClient.on('offline', () => { if (this.connected) { this.logger.log('info', 'MQTT went offline'); } this.connected = false; + setTimeout(() => this.setupMqtt(user, password), this.mqttReconnectInterval); }); - + this.MQTTClient.on('error', (err) => { this.logger.log('error', 'MQTT connection error:', err); this.connected = false; + setTimeout(() => this.setupMqtt(user, password), this.mqttReconnectInterval); }); } diff --git a/lib/connection.js b/lib/connection.js index f81585a..3b253aa 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -41,7 +41,8 @@ class InterfaceConnection extends EventEmitter { if (!this.connected) { this.logger.log('info', "Reconnecting to ", this.interfaceName); this.failureCount += 1; - this.initInterface(); + let backoffTime = Math.min(this.failureCount * this.reconnectInterval, 60000); // Cap at 60 seconds + setTimeout(() => this.initInterface(), backoffTime); } } From d946d12f6e11ca1c26640b114e4c292e9413ce84 Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Sun, 2 Jun 2024 11:15:55 +0200 Subject: [PATCH 09/14] added cleanup methods --- app.js | 10 +++++++++- lib/HomeMaticCCUJack.js | 15 +++++++++++++++ lib/connection.js | 21 +++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index a971f9a..a988dca 100644 --- a/app.js +++ b/app.js @@ -37,6 +37,15 @@ class Homematic extends Homey.App { } } + async onUninit() { + this.logger.log('info', 'Unloading homematic app...'); + Object.keys(this.bridges).forEach(serial => { + if (this.bridges[serial].cleanup) { + this.bridges[serial].cleanup(); + } + }); + } + getSettings() { return { "use_mqtt": this.homey.settings.get('use_mqtt'), @@ -110,7 +119,6 @@ class Homematic extends Homey.App { } catch (err) { this.logger.log('error', `Failed to initialize bridge ${bridge.serial}:`, err); } - } setBridgeAddress(serial, address) { diff --git a/lib/HomeMaticCCUJack.js b/lib/HomeMaticCCUJack.js index 4e4493a..12b5b20 100644 --- a/lib/HomeMaticCCUJack.js +++ b/lib/HomeMaticCCUJack.js @@ -39,6 +39,9 @@ class HomeMaticCCUJack extends EventEmitter { // Initialize request queue this.requestQueue = []; this.isProcessingQueue = false; + + // Ensure cleanup is called on application unload + this.homey.on('unload', () => this.cleanup()); } // Method to add request to the queue @@ -231,6 +234,18 @@ class HomeMaticCCUJack extends EventEmitter { }); }); } + + // Add this method to HomeMaticCCUJack class + cleanup() { + if (this.MQTTClient) { + this.MQTTClient.end(true, () => { + this.logger.log('info', 'MQTT client disconnected'); + }); + } + this.requestQueue = []; + this.isProcessingQueue = false; + } + } module.exports = HomeMaticCCUJack; diff --git a/lib/connection.js b/lib/connection.js index 3b253aa..9fd03f6 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -24,6 +24,9 @@ class InterfaceConnection extends EventEmitter { this.port = port; this.initRpcServerAndClient(port); }); + + // Ensure cleanup is called on application unload + Homey.app.on('unload', () => this.cleanup()); } getInitUrl() { @@ -179,6 +182,24 @@ class InterfaceConnection extends EventEmitter { }); }); } + + // Add this method to InterfaceConnection class + cleanup() { + if (this.rpcClient) { + this.rpcClient.methodCall('init', [this.getInitUrl(), ''], (err, res) => { + if (err) { + this.logger.log('error', 'Failed to cleanup RPC client:', err); + } + }); + } + if (this.rpcServer) { + this.rpcServer.close(() => { + this.logger.log('info', 'RPC server closed'); + }); + } + clearTimeout(this.rpcPingTimer); + clearInterval(this.retryConnectTimer); + } } function now() { From b4881341e3aecf142834b24d698fd0f762604a71 Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Sun, 2 Jun 2024 11:24:18 +0200 Subject: [PATCH 10/14] added batching reducing number of network calls --- lib/HomeMaticCCUJack.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/HomeMaticCCUJack.js b/lib/HomeMaticCCUJack.js index 12b5b20..f669343 100644 --- a/lib/HomeMaticCCUJack.js +++ b/lib/HomeMaticCCUJack.js @@ -58,9 +58,9 @@ class HomeMaticCCUJack extends EventEmitter { this.isProcessingQueue = true; while (this.requestQueue.length > 0) { - const request = this.requestQueue.shift(); + const requestBatch = this.requestQueue.splice(0, 10); // Batch of 10 requests try { - await request(); + await Promise.all(requestBatch.map(request => request())); } catch (error) { this.logger.log('error', 'Failed to process request:', error); } @@ -110,7 +110,7 @@ class HomeMaticCCUJack extends EventEmitter { this.emit(eventName, msg.v); } }); - + this.MQTTClient.on('close', () => { if (this.connected) { this.logger.log('info', 'MQTT disconnected'); @@ -118,7 +118,7 @@ class HomeMaticCCUJack extends EventEmitter { this.connected = false; setTimeout(() => this.setupMqtt(user, password), this.mqttReconnectInterval); }); - + this.MQTTClient.on('offline', () => { if (this.connected) { this.logger.log('info', 'MQTT went offline'); @@ -126,7 +126,7 @@ class HomeMaticCCUJack extends EventEmitter { this.connected = false; setTimeout(() => this.setupMqtt(user, password), this.mqttReconnectInterval); }); - + this.MQTTClient.on('error', (err) => { this.logger.log('error', 'MQTT connection error:', err); this.connected = false; @@ -245,7 +245,6 @@ class HomeMaticCCUJack extends EventEmitter { this.requestQueue = []; this.isProcessingQueue = false; } - } module.exports = HomeMaticCCUJack; From 2514cf693559429fd5035d64afe94f3f37fdae68 Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Sun, 2 Jun 2024 11:57:17 +0200 Subject: [PATCH 11/14] refactorings --- lib/DeviceLister.js | 70 ++++++++++++++++++++++++++++ lib/HomeMaticDiscovery.js | 90 +++++++++++++++++++----------------- lib/driver.js | 97 ++++++--------------------------------- 3 files changed, 133 insertions(+), 124 deletions(-) create mode 100644 lib/DeviceLister.js diff --git a/lib/DeviceLister.js b/lib/DeviceLister.js new file mode 100644 index 0000000..c22b2e6 --- /dev/null +++ b/lib/DeviceLister.js @@ -0,0 +1,70 @@ +'use strict'; + +class DeviceLister { + constructor(driver) { + this.driver = driver; + } + + async onListDevices(data, currentBridge) { + if (!currentBridge) { + return this.onListDevicesBridges(); + } + return this.onListDevicesDevices(currentBridge); + } + + async onListDevicesBridges() { + const self = this.driver; + try { + await self.homey.app.discovery.discover(); + return Object.values(self.homey.app.bridges).map(bridge => ({ + name: `CCU(${bridge.address})`, + data: { + serial: bridge.serial, + } + })); + } catch (err) { + throw new Error('Discovery failed'); + } + } + + async onListDevicesDevices(currentBridge) { + const self = this.driver; + if (!currentBridge) { + throw new Error('Missing Bridge'); + } + + let devices = []; + try { + const bridgeDevices = await currentBridge.listDevices(); + Object.keys(bridgeDevices).forEach(interfaceName => { + bridgeDevices[interfaceName].forEach(device => { + if (self.homematicTypes.includes(device.TYPE)) { + for (let idx = 0; idx < self.numDevices; idx++) { + const deviceObj = { + name: self.getDeviceName(device.ADDRESS, idx), + capabilities: self.capabilities, + data: { + id: device.ADDRESS, + attributes: { + HomeyInterfaceName: interfaceName, + bridgeSerial: currentBridge.serial + } + } + }; + if (self.multiDevice) { + deviceObj.data.attributes.Index = idx; + } + devices.push(deviceObj); + } + } + }); + }); + } catch (err) { + throw new Error('Failed to list devices: ' + err); + } + + return devices; + } +} + +module.exports = DeviceLister; diff --git a/lib/HomeMaticDiscovery.js b/lib/HomeMaticDiscovery.js index 8a85eb7..6c22552 100644 --- a/lib/HomeMaticDiscovery.js +++ b/lib/HomeMaticDiscovery.js @@ -4,17 +4,45 @@ const dgram = require('dgram'); const Constants = require('./constants'); const DISCOVER_MESSAGE = Buffer.from([0x02, 0x8F, 0x91, 0xC0, 0x01, 'e', 'Q', '3', 0x2D, 0x2A, 0x00, 0x2A, 0x00, 0x49]); -const DISCOVER_TIMEOUT = 5000; // Timeout for network delays +const DISCOVER_TIMEOUT = 5000; const CCU_PORT = 43439; const DGRAM_PORT = 48724; -module.exports = class HomeMaticDiscovery { - +class HomeMaticDiscovery { constructor(logger, homey) { this.logger = logger; this.homey = homey; this.devices = {}; - this.discoveryInProgress = false; // Flag to prevent multiple discovery processes + this.discoveryInProgress = false; + } + + async getClient() { + if (!this._client) { + this._client = this.createClient(); + } + return this._client; + } + + createClient() { + return new Promise((resolve, reject) => { + const client = dgram.createSocket('udp4'); + client.on('message', this._onClientMessage.bind(this)); + client.on('error', (err) => { + this.logger.log('error', 'UDP client error:', err); + }); + client.on('listening', () => { + const address = client.address(); + this.logger.log('info', `UDP client listening on ${address.address}:${address.port}`); + }); + client.bind(DGRAM_PORT, (err) => { + if (err) { + this.logger.log('error', 'Failed to bind UDP client:', err); + return reject(err); + } + client.setBroadcast(true); + resolve(client); + }); + }); } _onClientMessage(message, remote) { @@ -23,20 +51,7 @@ module.exports = class HomeMaticDiscovery { this.logger.log('info', `Raw message: ${message.toString('hex')}`); try { - const headerStart = 0; - const headerEnd = 5; - const typeStart = headerEnd; - const typeEnd = message.indexOf(0x00, typeStart); - if (typeEnd === -1) throw new Error('Invalid message format: missing type'); - - const type = message.slice(typeStart, typeEnd).toString(); - - const serialStart = typeEnd + 1; - const serialEnd = message.indexOf(0x00, serialStart); - if (serialEnd === -1) throw new Error('Invalid message format: missing serial'); - - const serial = message.slice(serialStart, serialEnd).toString(); - + const { type, serial } = this.parseMessage(message); this.logger.log('info', `Discovered device - Type: ${type}, Serial: ${serial}, Address: ${address}`); if (this.homey.app.bridges[serial]) { @@ -52,29 +67,20 @@ module.exports = class HomeMaticDiscovery { } } - async getClient() { - if (!this._client) { - this._client = new Promise((resolve, reject) => { - const client = dgram.createSocket('udp4'); - client.on('message', this._onClientMessage.bind(this)); - client.on('error', (err) => { - this.logger.log('error', 'UDP client error:', err); - }); - client.on('listening', () => { - const address = client.address(); - this.logger.log('info', `UDP client listening on ${address.address}:${address.port}`); - }); - client.bind(DGRAM_PORT, (err) => { - if (err) { - this.logger.log('error', 'Failed to bind UDP client:', err); - return reject(err); - } - client.setBroadcast(true); - resolve(client); - }); - }); - } - return this._client; + parseMessage(message) { + const headerEnd = 5; + const typeEnd = message.indexOf(0x00, headerEnd); + if (typeEnd === -1) throw new Error('Invalid message format: missing type'); + + const type = message.slice(headerEnd, typeEnd).toString(); + + const serialStart = typeEnd + 1; + const serialEnd = message.indexOf(0x00, serialStart); + if (serialEnd === -1) throw new Error('Invalid message format: missing serial'); + + const serial = message.slice(serialStart, serialEnd).toString(); + + return { type, serial }; } async discover({ timeout = DISCOVER_TIMEOUT } = {}) { @@ -105,3 +111,5 @@ module.exports = class HomeMaticDiscovery { } } } + +module.exports = HomeMaticDiscovery; diff --git a/lib/driver.js b/lib/driver.js index 0e3673b..ad73829 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -1,31 +1,34 @@ 'use strict'; const Homey = require('homey'); +const DeviceLister = require('./DeviceLister'); class Driver extends Homey.Driver { onInit() { this.multiDevice = false; this.numDevices = 1; + this.deviceLister = new DeviceLister(this); } async getBridge({ serial }) { - if (!serial && Object.keys(this.homey.app.bridges).length > 0) { - return this.homey.app.bridges[Object.keys(this.homey.app.bridges)[0]]; + const bridges = this.homey.app.bridges; + if (!serial && Object.keys(bridges).length > 0) { + return bridges[Object.keys(bridges)[0]]; } - if (this.homey.app.bridges[serial]) { - return this.homey.app.bridges[serial]; + if (bridges[serial]) { + return bridges[serial]; } await this.homey.app.discovery.discover(); - if (!serial && Object.keys(this.homey.app.bridges).length > 0) { - return this.homey.app.bridges[Object.keys(this.homey.app.bridges)[0]]; + if (!serial && Object.keys(bridges).length > 0) { + return bridges[Object.keys(bridges)[0]]; } - if (this.homey.app.bridges[serial]) { - return this.homey.app.bridges[serial]; + if (bridges[serial]) { + return bridges[serial]; } throw new Error('Bridge not found'); @@ -35,82 +38,10 @@ class Driver extends Homey.Driver { let currentBridge; const self = this; - const onListDevices = (data) => { - if (!currentBridge) { - return onListDevicesBridges(data); - } - return onListDevicesDevices(data); - }; - - const onListDevicesBridges = async (data) => { - try { - await self.homey.app.discovery.discover(); - const result = Object.values(self.homey.app.bridges).map(bridge => ({ - name: `CCU(${bridge.address})`, - data: { - serial: bridge.serial, - } - })); - - return result; - } catch (err) { - throw new Error('Discovery failed'); - } - }; - - const onListDevicesDevices = async (data) => { - if (!currentBridge) { - throw new Error('Missing Bridge'); - } - - let devices = []; - - try { - const bridgeDevices = await currentBridge.listDevices(); - Object.keys(bridgeDevices).forEach(interfaceName => { - bridgeDevices[interfaceName].forEach(device => { - if (self.homematicTypes.includes(device.TYPE)) { - for (let idx = 0; idx < this.numDevices; idx++) { - const deviceObj = { - name: this.getDeviceName(device.ADDRESS, idx), - capabilities: self.capabilities, - data: { - id: device.ADDRESS, - attributes: { - HomeyInterfaceName: interfaceName, - bridgeSerial: currentBridge.serial - } - } - }; - if (this.multiDevice) { - deviceObj.data.attributes.Index = idx; - } - devices.push(deviceObj); - } - } - }); - }); - } catch (err) { - throw new Error('Failed to list devices: ' + err); - } - - return devices; - }; - - const onListBridgesSelection = async (data) => { + session.setHandler('list_devices', (data) => self.deviceLister.onListDevices(data, currentBridge)); + session.setHandler('list_bridges_selection', (data) => { currentBridge = self.homey.app.bridges[data[0].data.serial]; - }; - - session.setHandler('list_devices', onListDevices); - session.setHandler('list_bridges_selection', onListBridgesSelection); - } - - getDeviceName(address, idx) { - if (this.multiDevice) { - return `${address}-${idx + 1}`; - } else { - return address; - } + }); } } From 6baed72ef0df2caa8bd7600abcabfdaa5a733dda Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Sun, 2 Jun 2024 14:19:59 +0200 Subject: [PATCH 12/14] refactorings --- lib/HomeMaticCCUJack.js | 443 ++++++++++++++++++++-------------------- lib/connection.js | 360 ++++++++++++++++---------------- 2 files changed, 397 insertions(+), 406 deletions(-) diff --git a/lib/HomeMaticCCUJack.js b/lib/HomeMaticCCUJack.js index f669343..f341a40 100644 --- a/lib/HomeMaticCCUJack.js +++ b/lib/HomeMaticCCUJack.js @@ -8,243 +8,236 @@ const Constants = require('./constants.js'); const mqttStatusPrefix = 'device/status/'; class HomeMaticCCUJack extends EventEmitter { - constructor(logger, homey, type, serial, ccuIP, mqttPort, user, password) { - super(); - this.homey = homey; - this.logger = logger; - this.ccuIP = ccuIP; - this.mqttPort = mqttPort; - this.type = type; - this.transport = Constants.TRANSPORT_MQTT; - this.serial = serial; - this.homeyIP = this.homey.app.homeyIP; - this.subscribedTopics = []; - this.connected = false; - this.setupMqtt(user, password); - this.mqttReconnectInterval = 5000; // Retry every 5 seconds - - let clientOptions = { - baseURL: 'http://' + ccuIP + ':2121/', - timeout: 10000 - }; - - if (user && password) { - clientOptions.auth = { - username: user, - password: password - }; - } - this.jackClient = axios.create(clientOptions); - - // Initialize request queue - this.requestQueue = []; - this.isProcessingQueue = false; - - // Ensure cleanup is called on application unload - this.homey.on('unload', () => this.cleanup()); - } - - // Method to add request to the queue - enqueueRequest(request) { - this.requestQueue.push(request); - this.processQueue(); + constructor(logger, homey, type, serial, ccuIP, mqttPort, user, password) { + super(); + this.homey = homey; + this.logger = logger; + this.ccuIP = ccuIP; + this.mqttPort = mqttPort; + this.type = type; + this.transport = Constants.TRANSPORT_MQTT; + this.serial = serial; + this.homeyIP = this.homey.app.homeyIP; + this.subscribedTopics = []; + this.connected = false; + this.setupMqtt(user, password); + this.mqttReconnectInterval = 5000; + + this.jackClient = this.createJackClient(ccuIP, user, password); + this.requestQueue = []; + this.isProcessingQueue = false; + + this.homey.on('unload', () => this.cleanup()); + } + + createJackClient(ccuIP, user, password) { + let clientOptions = { + baseURL: 'http://' + ccuIP + ':2121/', + timeout: 10000 + }; + + if (user && password) { + clientOptions.auth = { + username: user, + password: password + }; } + return axios.create(clientOptions); + } - // Method to process the request queue - async processQueue() { - if (this.isProcessingQueue || this.requestQueue.length === 0) { - return; - } + enqueueRequest(request) { + this.requestQueue.push(request); + this.processQueue(); + } - this.isProcessingQueue = true; - while (this.requestQueue.length > 0) { - const requestBatch = this.requestQueue.splice(0, 10); // Batch of 10 requests - try { - await Promise.all(requestBatch.map(request => request())); - } catch (error) { - this.logger.log('error', 'Failed to process request:', error); - } - } - this.isProcessingQueue = false; + async processQueue() { + if (this.isProcessingQueue || this.requestQueue.length === 0) { + return; } - subscribeTopic(name) { - this.enqueueRequest(() => new Promise((resolve, reject) => { - this.MQTTClient.subscribe(name, (err) => { - if (err) { - this.logger.log('error', 'Failed to subscribe to topic', name, err); - return reject(err); - } - this.logger.log('info', 'Subscribed to topic', name); - resolve(); - }); - })); + this.isProcessingQueue = true; + while (this.requestQueue.length > 0) { + const requestBatch = this.requestQueue.splice(0, 10); // Batch of 10 requests + try { + await Promise.all(requestBatch.map(request => request())); + } catch (error) { + this.logger.log('error', 'Failed to process request:', error); + } } - - setupMqtt(user, password) { - let options = {}; - if (user && password) { - options.username = user; - options.password = password; + this.isProcessingQueue = false; + } + + subscribeTopic(name) { + this.enqueueRequest(() => new Promise((resolve, reject) => { + this.MQTTClient.subscribe(name, (err) => { + if (err) { + this.logger.log('error', 'Failed to subscribe to topic', name, err); + return reject(err); } - this.MQTTClient = mqtt.connect('mqtt://' + this.ccuIP + ':' + this.mqttPort, options); - this.MQTTClient.on('connect', () => { - this.logger.log('info', 'Connected to CCU Jack broker at:', this.ccuIP + ':' + this.mqttPort); - this.connected = true; - this.subscribedTopics.forEach(topic => { - this.subscribeTopic(topic); - }); - }); - - this.MQTTClient.on('message', (topic, message) => { - if (topic.startsWith(mqttStatusPrefix)) { - let devTopic = topic.replace(mqttStatusPrefix, ''); - let parts = devTopic.split('/'); - if (parts.length !== 3) { - this.logger.log('error', 'Received malformed message', topic, devTopic, parts); - return; - } - let [address, channel, datapoint] = parts; - let msg = JSON.parse(message.toString()); - let eventName = this.getEventName(address, channel, datapoint); - this.emit(eventName, msg.v); - } - }); - - this.MQTTClient.on('close', () => { - if (this.connected) { - this.logger.log('info', 'MQTT disconnected'); - } - this.connected = false; - setTimeout(() => this.setupMqtt(user, password), this.mqttReconnectInterval); - }); - - this.MQTTClient.on('offline', () => { - if (this.connected) { - this.logger.log('info', 'MQTT went offline'); - } - this.connected = false; - setTimeout(() => this.setupMqtt(user, password), this.mqttReconnectInterval); - }); - - this.MQTTClient.on('error', (err) => { - this.logger.log('error', 'MQTT connection error:', err); - this.connected = false; - setTimeout(() => this.setupMqtt(user, password), this.mqttReconnectInterval); - }); - } - - getEventName(address, channel, datapoint) { - return "event-" + address + ":" + channel + "-" + datapoint; + this.logger.log('info', 'Subscribed to topic', name); + resolve(); + }); + })); + } + + setupMqtt(user, password) { + let options = {}; + if (user && password) { + options.username = user; + options.password = password; } - - on(event, callback) { - super.on(event, callback); - - const prefix = "event-"; - if (event.startsWith(prefix)) { - let tmp = event.replace(prefix, ""); - let [channel, datapoint] = tmp.split("-"); - channel = channel.replace(":", "/"); - let topic = mqttStatusPrefix + channel + '/' + datapoint; - if (!this.subscribedTopics.includes(topic)) { - this.subscribedTopics.push(topic); - } - if (this.connected) { - this.subscribeTopic(topic); - } + this.MQTTClient = mqtt.connect('mqtt://' + this.ccuIP + ':' + this.mqttPort, options); + this.MQTTClient.on('connect', () => { + this.logger.log('info', 'Connected to CCU Jack broker at:', this.ccuIP + ':' + this.mqttPort); + this.connected = true; + this.subscribedTopics.forEach(topic => { + this.subscribeTopic(topic); + }); + }); + + this.MQTTClient.on('message', (topic, message) => { + if (topic.startsWith(mqttStatusPrefix)) { + let devTopic = topic.replace(mqttStatusPrefix, ''); + let parts = devTopic.split('/'); + if (parts.length !== 3) { + this.logger.log('error', 'Received malformed message', topic, devTopic, parts); + return; } + let [address, channel, datapoint] = parts; + let msg = JSON.parse(message.toString()); + let eventName = this.getEventName(address, channel, datapoint); + this.emit(eventName, msg.v); + } + }); + + this.MQTTClient.on('close', () => { + if (this.connected) { + this.logger.log('info', 'MQTT disconnected'); + } + this.connected = false; + setTimeout(() => this.setupMqtt(user, password), this.mqttReconnectInterval); + }); + + this.MQTTClient.on('offline', () => { + if (this.connected) { + this.logger.log('info', 'MQTT went offline'); + } + this.connected = false; + setTimeout(() => this.setupMqtt(user, password), this.mqttReconnectInterval); + }); + + this.MQTTClient.on('error', (err) => { + this.logger.log('error', 'MQTT connection error:', err); + this.connected = false; + setTimeout(() => this.setupMqtt(user, password), this.mqttReconnectInterval); + }); + } + + getEventName(address, channel, datapoint) { + return "event-" + address + ":" + channel + "-" + datapoint; + } + + on(event, callback) { + super.on(event, callback); + + const prefix = "event-"; + if (event.startsWith(prefix)) { + let tmp = event.replace(prefix, ""); + let [channel, datapoint] = tmp.split("-"); + channel = channel.replace(":", "/"); + let topic = mqttStatusPrefix + channel + '/' + datapoint; + if (!this.subscribedTopics.includes(topic)) { + this.subscribedTopics.push(topic); + } + if (this.connected) { + this.subscribeTopic(topic); + } } - - async listDevices() { - return new Promise((resolve, reject) => { - this.enqueueRequest(() => { - return this.jackClient.get('device') - .then(resp => { - let deviceLinks = resp.data['~links']; - let devPromises = []; - deviceLinks.forEach(link => { - let p = this.jackClient.get('device/' + link['href']); - devPromises.push(p); - }); - - return Promise.all(devPromises) - .then(results => { - let devices = {}; - results.forEach(dev => { - let device = dev.data; - if (!devices[device.interfaceType]) { - devices[device.interfaceType] = []; - } - devices[device.interfaceType].push({ - TYPE: device.type, - ADDRESS: device.address - }); - }); - resolve(devices); - }) - .catch(err => { - this.logger.log("error", "Failed to process devices:", err); - reject(err); - }); - }) - .catch(err => { - this.logger.log("error", "Failed to get devices:", err); - reject(err); - }); - }); - }); + } + + async listDevices() { + return new Promise((resolve, reject) => { + this.enqueueRequest(() => { + return this.jackClient.get('device') + .then(resp => { + let deviceLinks = resp.data['~links']; + let devPromises = deviceLinks.map(link => this.jackClient.get('device/' + link['href'])); + return Promise.all(devPromises) + .then(results => { + let devices = {}; + results.forEach(dev => { + let device = dev.data; + if (!devices[device.interfaceType]) { + devices[device.interfaceType] = []; + } + devices[device.interfaceType].push({ + TYPE: device.type, + ADDRESS: device.address + }); + }); + resolve(devices); + }) + .catch(err => { + this.logger.log("error", "Failed to process devices:", err); + reject(err); + }); + }) + .catch(err => { + this.logger.log("error", "Failed to get devices:", err); + reject(err); + }); + }); + }); + } + + async getValue(interfaceName, address, key) { + return new Promise((resolve, reject) => { + let valueURL = 'device/' + address.replace(':', '/') + '/' + key + '/~pv'; + this.enqueueRequest(() => { + return this.jackClient.get(valueURL) + .then(resp => { + resolve(resp.data.v); + }) + .catch(err => { + this.logger.log("error", "Failed to get device value:", address, key, err); + reject(new Error('Failed to get value')); + }); + }); + }); + } + + async setValue(interfaceName, address, key, value) { + let myValue = value; + if (myValue === "1.0") { + myValue = 1; + } else if (myValue === "0.0") { + myValue = 0; } - - async getValue(interfaceName, address, key) { - return new Promise((resolve, reject) => { - let valueURL = 'device/' + address.replace(':', '/') + '/' + key + '/~pv'; - this.enqueueRequest(() => { - return this.jackClient.get(valueURL) - .then(resp => { - resolve(resp.data.v); - }) - .catch(err => { - this.logger.log("error", "Failed to get device value:", address, key, err); - reject(new Error('Failed to get value')); - }); - }); - }); - } - - async setValue(interfaceName, address, key, value) { - let myValue = value; - if (myValue === "1.0") { - myValue = 1; - } else if (myValue === "0.0") { - myValue = 0; - } - return new Promise((resolve, reject) => { - this.enqueueRequest(() => { - return new Promise((resolveInner, rejectInner) => { - this.MQTTClient.publish('device/set/' + address.replace(':', '/') + '/' + key, JSON.stringify(myValue), (err) => { - if (err) { - this.logger.log('error', 'Failed to publish message:', err); - rejectInner(new Error('Failed to set value')); - } else { - resolveInner(true); - } - }); - }).then(resolve).catch(reject); - }); - }); - } - - // Add this method to HomeMaticCCUJack class - cleanup() { - if (this.MQTTClient) { - this.MQTTClient.end(true, () => { - this.logger.log('info', 'MQTT client disconnected'); - }); - } - this.requestQueue = []; - this.isProcessingQueue = false; + return new Promise((resolve, reject) => { + this.enqueueRequest(() => { + return new Promise((resolveInner, rejectInner) => { + this.MQTTClient.publish('device/set/' + address.replace(':', '/') + '/' + key, JSON.stringify(myValue), (err) => { + if (err) { + this.logger.log('error', 'Failed to publish message:', err); + rejectInner(new Error('Failed to set value')); + } else { + resolveInner(true); + } + }); + }).then(resolve).catch(reject); + }); + }); + } + + cleanup() { + if (this.MQTTClient) { + this.MQTTClient.end(true, () => { + this.logger.log('info', 'MQTT client disconnected'); + }); } + this.requestQueue = []; + this.isProcessingQueue = false; + } } module.exports = HomeMaticCCUJack; diff --git a/lib/connection.js b/lib/connection.js index 9fd03f6..36e6406 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -6,204 +6,202 @@ const Homey = require('homey'); class InterfaceConnection extends EventEmitter { - constructor(logger, interfaceName, config) { - super(); - this.logger = logger; - this.interfaceName = interfaceName; - this._settings = config; - this.failureCount = 0; - this.maxFailureCount = 10; - this.deactivated = false; - this.connected = false; - this.wasConnected = false; - this.reconnectInterval = 10000; - this.retrying = false; - - freeport((err, port) => { - if (err) throw err; - this.port = port; - this.initRpcServerAndClient(port); - }); - - // Ensure cleanup is called on application unload - Homey.app.on('unload', () => this.cleanup()); + constructor(logger, interfaceName, config) { + super(); + this.logger = logger; + this.interfaceName = interfaceName; + this._settings = config; + this.failureCount = 0; + this.maxFailureCount = 10; + this.deactivated = false; + this.connected = false; + this.wasConnected = false; + this.reconnectInterval = 10000; + this.retrying = false; + + freeport((err, port) => { + if (err) throw err; + this.port = port; + this.initRpcServerAndClient(port); + }); + + Homey.app.on('unload', () => this.cleanup()); + } + + getInitUrl() { + return `${this._settings.protocol}://${this._settings.homeyIP}:${this.port}`; + } + + retryConnect() { + this.logger.log('info', "Check retry: ", this.interfaceName); + if (this.failureCount > this.maxFailureCount && !this.wasConnected) { + this.logger.log('info', "Giving up on ", this.interfaceName); + clearInterval(this.retryConnectTimer); + this.retrying = false; + return; } - - getInitUrl() { - return `${this._settings.protocol}://${this._settings.homeyIP}:${this.port}`; + if (!this.connected) { + this.logger.log('info', "Reconnecting to ", this.interfaceName); + this.failureCount += 1; + let backoffTime = Math.min(this.failureCount * this.reconnectInterval, 60000); // Cap at 60 seconds + setTimeout(() => this.initInterface(), backoffTime); } - - retryConnect() { - this.logger.log('info', "Check retry: ", this.interfaceName); - if (this.failureCount > this.maxFailureCount && !this.wasConnected) { - this.logger.log('info', "Giving up on ", this.interfaceName); - clearInterval(this.retryConnectTimer); - this.retrying = false; - return; + } + + initRpcServerAndClient(port) { + this.createRpcServer(port); + this.rpcClient = this.createRpcClient(); + this.initInterface(); + } + + createRpcServer(port) { + this.rpcServer = this._settings.rpc.createServer({ host: this._settings.homeyIP, port }); + + this.rpcServer.on('system.listMethods', (err, params, callback) => { + callback(null, ['system.listMethods', 'system.multicall', 'event', 'listDevices']); + }); + + this.rpcServer.on('listDevices', (err, params, callback) => { + callback(null, []); + }); + + this.rpcServer.on('event', (err, params, callback) => { + this.emitEvent(params); + callback(null, ''); + }); + + this.rpcServer.on('system.multicall', (err, params, callback) => { + params[0].forEach(call => { + if (call.methodName === 'event') { + this.emitEvent(call.params); } - if (!this.connected) { - this.logger.log('info', "Reconnecting to ", this.interfaceName); - this.failureCount += 1; - let backoffTime = Math.min(this.failureCount * this.reconnectInterval, 60000); // Cap at 60 seconds - setTimeout(() => this.initInterface(), backoffTime); - } - } - - initRpcServerAndClient(port) { - this.createRpcServer(port); - this.rpcClient = this.createRpcClient(); - this.initInterface(); - } - - createRpcServer(port) { - this.rpcServer = this._settings.rpc.createServer({ host: this._settings.homeyIP, port }); - - this.rpcServer.on('system.listMethods', (err, params, callback) => { - callback(null, ['system.listMethods', 'system.multicall', 'event', 'listDevices']); - }); - - this.rpcServer.on('listDevices', (err, params, callback) => { - callback(null, []); - }); - - this.rpcServer.on('event', (err, params, callback) => { - this.emitEvent(params); - callback(null, ''); - }); - - this.rpcServer.on('system.multicall', (err, params, callback) => { - params[0].forEach(call => { - if (call.methodName === 'event') { - this.emitEvent(call.params); - } - }); - callback(null, ''); - }); - } - - createRpcClient() { - return this._settings.rpc.createClient({ host: this._settings.ccuIP, port: this._settings.port }); + }); + callback(null, ''); + }); + } + + createRpcClient() { + return this._settings.rpc.createClient({ host: this._settings.ccuIP, port: this._settings.port }); + } + + emitEvent(event) { + if (event && event.length === 4) { + const eventName = `event-${event[1]}-${event[2]}`; + this.emit('event', { + name: eventName, + value: event[3] + }); } + this.lastEvent = now(); + } - emitEvent(event) { - if (event && event.length === 4) { - const eventName = `event-${event[1]}-${event[2]}`; - this.emit('event', { - name: eventName, - value: event[3] - }); + initInterface() { + this.rpcClient.methodCall('init', [this.getInitUrl(), `homey_${this.interfaceName}`], (err, res) => { + if (err) { + this.logger.log('info', "Failed to connect:", this.interfaceName, err); + this.connected = false; + if (!this.retrying) { + this.retrying = true; + this.retryConnectTimer = setInterval(() => this.retryConnect(), this.reconnectInterval); } + } else { + this.logger.log('info', "Connected to", this.interfaceName); + this.failureCount = 0; + this.connected = true; + this.wasConnected = true; + this.retrying = false; + clearInterval(this.retryConnectTimer); this.lastEvent = now(); + this.checkConnection(); + } + }); + } + + checkConnection() { + clearTimeout(this.rpcPingTimer); + const pingTimeout = this._settings.pingTimeout; + const elapsed = Math.round((now() - this.lastEvent) / 1000); + + if (elapsed > pingTimeout) { + this.logger.log('info', 'ping timeout', this.interfaceName, elapsed); + this.initInterface(); + return; } - - initInterface() { - this.rpcClient.methodCall('init', [this.getInitUrl(), `homey_${this.interfaceName}`], (err, res) => { - if (err) { - this.logger.log('info', "Failed to connect:", this.interfaceName, err); - this.connected = false; - if (!this.retrying) { - this.retrying = true; - this.retryConnectTimer = setInterval(() => this.retryConnect(), this.reconnectInterval); - } - } else { - this.logger.log('info', "Connected to", this.interfaceName); - this.failureCount = 0; - this.connected = true; - this.wasConnected = true; - this.retrying = false; - clearInterval(this.retryConnectTimer); - this.lastEvent = now(); - this.checkConnection(); - } - }); - } - - checkConnection() { - clearTimeout(this.rpcPingTimer); - const pingTimeout = this._settings.pingTimeout; - const elapsed = Math.round((now() - this.lastEvent) / 1000); - - if (elapsed > pingTimeout) { - this.logger.log('info', 'ping timeout', this.interfaceName, elapsed); - this.initInterface(); - return; - } - if (elapsed >= (pingTimeout / 2)) { - this.logger.log('info', "Sending ping to ", this.interfaceName); - this.rpcClient.methodCall('ping', ['homey'], (err, res) => { }); - } - this.rpcPingTimer = setTimeout(() => this.checkConnection(), pingTimeout * 250); - } - - unsubscribe() { - this.rpcClient.methodCall('init', [this.getInitUrl(), ''], (err, res) => { }); - } - - listDevices() { - return new Promise((resolve, reject) => { - if (!this.connected) { - resolve({ 'interfaceName': this.interfaceName, 'devices': [] }); - } else { - this.rpcClient.methodCall('listDevices', [], (err, res) => { - if (err) { - reject(err); - } else { - const devices = res.map(device => { - device.HomeyInterfaceName = this.interfaceName; - return device; - }); - resolve({ 'interfaceName': this.interfaceName, 'devices': devices }); - } - }); - } - }); - } - - getValue(address, key) { - return new Promise((resolve, reject) => { - this.rpcClient.methodCall('getValue', [address, key], (err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }); + if (elapsed >= (pingTimeout / 2)) { + this.logger.log('info', "Sending ping to ", this.interfaceName); + this.rpcClient.methodCall('ping', ['homey'], (err, res) => { }); } - - setValue(address, key, value) { - return new Promise((resolve, reject) => { - this.rpcClient.methodCall('setValue', [address, key, value], (err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } + this.rpcPingTimer = setTimeout(() => this.checkConnection(), pingTimeout * 250); + } + + unsubscribe() { + this.rpcClient.methodCall('init', [this.getInitUrl(), ''], (err, res) => { }); + } + + listDevices() { + return new Promise((resolve, reject) => { + if (!this.connected) { + resolve({ 'interfaceName': this.interfaceName, 'devices': [] }); + } else { + this.rpcClient.methodCall('listDevices', [], (err, res) => { + if (err) { + reject(err); + } else { + const devices = res.map(device => { + device.HomeyInterfaceName = this.interfaceName; + return device; }); + resolve({ 'interfaceName': this.interfaceName, 'devices': devices }); + } }); - } - - // Add this method to InterfaceConnection class - cleanup() { - if (this.rpcClient) { - this.rpcClient.methodCall('init', [this.getInitUrl(), ''], (err, res) => { - if (err) { - this.logger.log('error', 'Failed to cleanup RPC client:', err); - } - }); + } + }); + } + + getValue(address, key) { + return new Promise((resolve, reject) => { + this.rpcClient.methodCall('getValue', [address, key], (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); } - if (this.rpcServer) { - this.rpcServer.close(() => { - this.logger.log('info', 'RPC server closed'); - }); + }); + }); + } + + setValue(address, key, value) { + return new Promise((resolve, reject) => { + this.rpcClient.methodCall('setValue', [address, key, value], (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); } - clearTimeout(this.rpcPingTimer); - clearInterval(this.retryConnectTimer); + }); + }); + } + + cleanup() { + if (this.rpcClient) { + this.rpcClient.methodCall('init', [this.getInitUrl(), ''], (err, res) => { + if (err) { + this.logger.log('error', 'Failed to cleanup RPC client:', err); + } + }); + } + if (this.rpcServer) { + this.rpcServer.close(() => { + this.logger.log('info', 'RPC server closed'); + }); } + clearTimeout(this.rpcPingTimer); + clearInterval(this.retryConnectTimer); + } } function now() { - return (new Date()).getTime(); + return (new Date()).getTime(); } module.exports = InterfaceConnection; From c7ef03736be7070becf956e3692fcb81073adb99 Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Sun, 2 Jun 2024 20:46:15 +0200 Subject: [PATCH 13/14] refactoring --- lib/CapabilityManager.js | 131 ++++++++++++++++++++++++++++++++++ lib/device.js | 150 ++++++--------------------------------- 2 files changed, 152 insertions(+), 129 deletions(-) create mode 100644 lib/CapabilityManager.js diff --git a/lib/CapabilityManager.js b/lib/CapabilityManager.js new file mode 100644 index 0000000..0243d51 --- /dev/null +++ b/lib/CapabilityManager.js @@ -0,0 +1,131 @@ +'use strict'; + +const Constants = require('./constants'); + +class CapabilityManager { + constructor(device, capabilityMap) { + this.device = device; + this.capabilityMap = capabilityMap; + this.deviceAddress = device.deviceAddress; + this.HomeyInterfaceName = device.HomeyInterfaceName; + } + + registerCapabilityListeners() { + Object.keys(this.capabilityMap).forEach(capabilityName => { + const capability = this.capabilityMap[capabilityName]; + this.device.logger.log('info', `Registering capability listener for ${capabilityName}`); + + if (capability.set) { + this.device.registerCapabilityListener(capabilityName, async (value, opts) => { + let setValue = value; + if (capability.set.convertMQTT && this.device.bridge.transport === Constants.TRANSPORT_MQTT) { + setValue = capability.set.convertMQTT(value); + } else if (capability.set.convert) { + setValue = capability.set.convert(value); + } else { + setValue = this.convertValue(capability.set.valueType, value); + } + + let key = capability.set.key; + if (capability.set.convertKey) { + key = capability.set.convertKey(key, value); + } + + let channel = capability.set.channel; + if (capability.set.convertChannel) { + channel = capability.set.convertChannel(channel, value); + } + + this.device.setValue(channel, key, setValue); + }); + } + }); + } + + initializeCapabilities() { + Object.keys(this.capabilityMap).forEach(name => { + const capability = this.capabilityMap[name]; + if (capability.channel && capability.key) { + this.getCapabilityValue(name); + this.initializeEventListener(name); + } + }); + this.initializeExtraEventListeners(); + } + + getCapabilityValue(capabilityName) { + const capability = this.capabilityMap[capabilityName]; + this.device.bridge.getValue(this.HomeyInterfaceName, `${this.deviceAddress}:${capability.channel}`, capability.key) + .then(value => { + value = this.convertCapabilityValue(value, capabilityName); + return this.device.setCapabilityValue(capabilityName, value); + }) + .catch(err => { + this.device.logger.log('error', `Failed to get capability ${capabilityName} for device ${this.device.getName()} (${this.deviceAddress})`, err); + }); + } + + initializeEventListener(capabilityName) { + const capability = this.capabilityMap[capabilityName]; + const eventName = `event-${this.deviceAddress}:${capability.channel}-${capability.key}`; + + this.device.logger.log('info', `Initializing event listener for ${capabilityName} with event name ${eventName}`); + this.device.bridge.on(eventName, async value => { + value = this.convertCapabilityValue(value, capabilityName); + if (value !== undefined) { + try { + await this.device.setCapabilityValue(capabilityName, value); + } catch (err) { + this.device.logger.log('error', `Failed to set capability ${capabilityName} for device ${this.device.getName()} (${this.deviceAddress})`, err); + } + } + }); + this.device.addedEvents.push(eventName); + } + + initializeExtraEventListeners() { + // Implement additional event listeners if needed + } + + convertCapabilityValue(value, capabilityName) { + const { convert, convertMQTT, valueType } = this.capabilityMap[capabilityName]; + if (convertMQTT && this.device.bridge.transport === Constants.TRANSPORT_MQTT) { + return convertMQTT(value); + } else if (convert) { + return convert(value); + } else { + return this.convertValue(valueType, value); + } + } + + convertValue(valueType, value) { + switch (valueType) { + case 'string': + return value.toString(); + case 'int': + return parseInt(value); + case 'float': + return parseFloat(value); + case 'boolean': + return value === 1; + case 'onOffDimGet': + return value > 0; + case 'keymatic': + return true; + case 'keymatic_swap': + return !value; + case 'onOffDimSet': + return value ? 0.99 : "0.0"; + case 'Wh': + return parseFloat(value) / 1000; + case 'floatPercent': + return parseFloat(value) * 100; + case 'mA': + return parseFloat(value) / 1000; + default: + return value; + } + } +} + +module.exports = CapabilityManager; diff --git a/lib/device.js b/lib/device.js index 54320fe..86745e5 100644 --- a/lib/device.js +++ b/lib/device.js @@ -1,36 +1,35 @@ 'use strict'; const Homey = require('homey'); -const Constants = require('./constants.js'); +const CapabilityManager = require('./CapabilityManager'); class Device extends Homey.Device { - onInit(capabilityMap) { + async onInit(capabilityMap) { this.logger = this.homey.app.logger; + this.logger.log('info', `Initializing device with capability map: ${JSON.stringify(capabilityMap)}`); this.capabilityMap = capabilityMap; this.deviceAddress = this.getData().id; this.HomeyInterfaceName = this.getData().attributes.HomeyInterfaceName; - this.bridgeSerial = this.getSetting('ccuSerial'); - if (!this.bridgeSerial) { - this.bridgeSerial = this.getData().attributes.bridgeSerial; - } + this.bridgeSerial = this.getSetting('ccuSerial') || this.getData().attributes.bridgeSerial; this.addedEvents = []; - this.driver.getBridge({ serial: this.bridgeSerial }) - .then(bridge => { - this.bridge = bridge; - this.initilizeCapabilities(); - this.registerCapabilityListeners(); - return this.setSettings({ - address: this.deviceAddress, - ccuIP: this.bridge.ccuIP, - ccuSerial: this.bridge.serial, - driver: this.driver.manifest.id - }); - }) - .catch(err => { - this.error('Failed to initialize device:', err); + try { + this.bridge = await this.driver.getBridge({ serial: this.bridgeSerial }); + this.logger.log('info', `Bridge found: ${this.bridgeSerial}`); + this.capabilityManager = new CapabilityManager(this, this.capabilityMap); // Moved this line here + this.capabilityManager.initializeCapabilities(); + this.capabilityManager.registerCapabilityListeners(); + + await this.setSettings({ + address: this.deviceAddress, + ccuIP: this.bridge.ccuIP, + ccuSerial: this.bridge.serial, + driver: this.driver.manifest.id }); + } catch (err) { + this.error('Failed to initialize device:', err); + } } onDeleted() { @@ -42,117 +41,10 @@ class Device extends Homey.Device { setValue(channel, key, value) { this.bridge.setValue(this.HomeyInterfaceName, `${this.deviceAddress}:${channel}`, key, value) .catch(err => { - this.logger.log('info', 'Set', key, 'failed for device - Value', value, this.deviceAddress); - throw new Error('Failed to set value'); + this.logger.log('info', 'Set', key, 'failed for device - Value', value, this.deviceAddress); + throw new Error('Failed to set value'); }); } - - registerCapabilityListeners() { - Object.keys(this.capabilityMap).forEach(capabilityName => { - if (this.capabilityMap[capabilityName].set) { - this.registerCapabilityListener(capabilityName, async (value, opts) => { - let setValue = value; - if (this.capabilityMap[capabilityName].set.convertMQTT && this.bridge.transport === Constants.TRANSPORT_MQTT) { - setValue = this.capabilityMap[capabilityName].set.convertMQTT(value); - } else if (this.capabilityMap[capabilityName].set.convert) { - setValue = this.capabilityMap[capabilityName].set.convert(value); - } else { - setValue = this.convertValue(this.capabilityMap[capabilityName].set.valueType, value); - } - - let key = this.capabilityMap[capabilityName].set.key; - if (this.capabilityMap[capabilityName].set.convertKey) { - key = this.capabilityMap[capabilityName].set.convertKey(key, value); - } - - let channel = this.capabilityMap[capabilityName].set.channel; - if (this.capabilityMap[capabilityName].set.convertChannel) { - channel = this.capabilityMap[capabilityName].set.convertChannel(channel, value); - } - - this.setValue(channel, key, setValue); - }); - } - }); - } - - initilizeCapabilities() { - Object.keys(this.capabilityMap).forEach(name => { - if (this.capabilityMap[name].channel && this.capabilityMap[name].key) { - this.getCapabilityValue(name); - this.initializeEventListener(name); - } - }); - this.initializeExtraEventListeners(); - } - - getCapabilityValue(capabilityName) { - this.bridge.getValue(this.HomeyInterfaceName, `${this.deviceAddress}:${this.capabilityMap[capabilityName].channel}`, this.capabilityMap[capabilityName].key) - .then(value => { - value = this.convertCapabilityValue(value, capabilityName); - return this.setCapabilityValue(capabilityName, value); - }) - .catch(err => { - this.logger.log('error', `Failed to get capability ${capabilityName} for device ${this.getName()} (${this.deviceAddress})`, err); - }); - } - - initializeEventListener(capabilityName) { - const eventName = `event-${this.deviceAddress}:${this.capabilityMap[capabilityName].channel}-${this.capabilityMap[capabilityName].key}`; - this.bridge.on(eventName, async value => { - value = this.convertCapabilityValue(value, capabilityName); - if (value !== undefined) { - await this.setCapabilityValue(capabilityName, value).catch(err => { - this.logger.log('error', `Failed to set capability ${capabilityName} for device ${this.getName()} (${this.deviceAddress})`, err); - }); - } - }); - this.addedEvents.push(eventName); - } - - initializeExtraEventListeners() { - // Implement additional event listeners if needed - } - - convertCapabilityValue(value, capabilityName) { - const { convert, convertMQTT, valueType } = this.capabilityMap[capabilityName]; - if (convertMQTT && this.bridge.transport === Constants.TRANSPORT_MQTT) { - return convertMQTT(value); - } else if (convert) { - return convert(value); - } else { - return this.convertValue(valueType, value); - } - } - - convertValue(valueType, value) { - switch (valueType) { - case 'string': - return value.toString(); - case 'int': - return parseInt(value); - case 'float': - return parseFloat(value); - case 'boolean': - return value === 1; - case 'onOffDimGet': - return value > 0; - case 'keymatic': - return true; - case 'keymatic_swap': - return !value; - case 'onOffDimSet': - return value ? 0.99 : "0.0"; - case 'Wh': - return parseFloat(value) / 1000; - case 'floatPercent': - return parseFloat(value) * 100; - case 'mA': - return parseFloat(value) / 1000; - default: - return value; - } - } } module.exports = Device; From d2a73649aad73c52d8dadfa2ec1c04c8cb9a574a Mon Sep 17 00:00:00 2001 From: Robin Baum Date: Mon, 3 Jun 2024 22:31:45 +0200 Subject: [PATCH 14/14] bugfix --- lib/driver.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/driver.js b/lib/driver.js index ad73829..4e2b157 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -11,6 +11,14 @@ class Driver extends Homey.Driver { this.deviceLister = new DeviceLister(this); } + getDeviceName(address, idx) { + if (this.multiDevice === true) { + return `${address}-${idx + 1}`; + } else { + return address; + } + } + async getBridge({ serial }) { const bridges = this.homey.app.bridges; if (!serial && Object.keys(bridges).length > 0) {