@switchbot/homebridge-switchbot 5.0.0-beta.13 → 5.0.0-beta.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/e2e-verification.md +36 -0
- package/.github/copilot-instructions.md +39 -0
- package/.github/workflows/ci.yml +32 -0
- package/.github/workflows/manual-e2e.yml +115 -0
- package/.github/workflows/release.yml +0 -4
- package/CHANGELOG.md +35 -0
- package/E2E-VERIFICATION.md +121 -0
- package/MIGRATION.md +54 -0
- package/README.md +99 -3
- package/TODO.md +263 -0
- package/config.schema.json +198 -14787
- package/dist/device-types.d.ts +31 -0
- package/dist/device-types.d.ts.map +1 -0
- package/dist/device-types.js +246 -0
- package/dist/device-types.js.map +1 -0
- package/dist/deviceFactory.d.ts +13 -0
- package/dist/deviceFactory.d.ts.map +1 -0
- package/dist/deviceFactory.js +165 -0
- package/dist/deviceFactory.js.map +1 -0
- package/dist/devices/deviceBase.d.ts +50 -0
- package/dist/devices/deviceBase.d.ts.map +1 -0
- package/dist/devices/deviceBase.js +119 -0
- package/dist/devices/deviceBase.js.map +1 -0
- package/dist/devices/genericDevice.d.ts +277 -0
- package/dist/devices/genericDevice.d.ts.map +1 -0
- package/dist/devices/genericDevice.js +1396 -0
- package/dist/devices/genericDevice.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/homebridge-ui/endpoints/config.d.ts +3 -0
- package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/config.js +107 -0
- package/dist/homebridge-ui/endpoints/config.js.map +1 -0
- package/dist/homebridge-ui/endpoints/credentials.d.ts +6 -0
- package/dist/homebridge-ui/endpoints/credentials.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/credentials.js +65 -0
- package/dist/homebridge-ui/endpoints/credentials.js.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.js +430 -0
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
- package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/discovery.js +212 -0
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
- package/dist/homebridge-ui/public/css/styles.css +447 -0
- package/dist/homebridge-ui/public/index.html +144 -248
- package/dist/homebridge-ui/public/js/api.d.ts +62 -0
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/api.js +225 -0
- package/dist/homebridge-ui/public/js/api.js.map +1 -0
- package/dist/homebridge-ui/public/js/api.ts +279 -0
- package/dist/homebridge-ui/public/js/app.d.ts +2 -0
- package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/app.js +3600 -0
- package/dist/homebridge-ui/public/js/app.js.map +7 -0
- package/dist/homebridge-ui/public/js/app.ts +23 -0
- package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
- package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/constants.js +2 -0
- package/dist/homebridge-ui/public/js/constants.js.map +1 -0
- package/dist/homebridge-ui/public/js/constants.ts +1 -0
- package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
- package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/credentials.js +96 -0
- package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
- package/dist/homebridge-ui/public/js/credentials.ts +100 -0
- package/dist/homebridge-ui/public/js/devices-delete.d.ts +3 -0
- package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/devices-delete.js +206 -0
- package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
- package/dist/homebridge-ui/public/js/devices-delete.ts +236 -0
- package/dist/homebridge-ui/public/js/devices.d.ts +9 -0
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/devices.js +94 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -0
- package/dist/homebridge-ui/public/js/devices.ts +114 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts +4 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/discovery.js +1364 -0
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
- package/dist/homebridge-ui/public/js/discovery.ts +1492 -0
- package/dist/homebridge-ui/public/js/logger.d.ts +6 -0
- package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/logger.js +12 -0
- package/dist/homebridge-ui/public/js/logger.js.map +1 -0
- package/dist/homebridge-ui/public/js/logger.ts +11 -0
- package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
- package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/modal.js +26 -0
- package/dist/homebridge-ui/public/js/modal.js.map +1 -0
- package/dist/homebridge-ui/public/js/modal.ts +28 -0
- package/dist/homebridge-ui/public/js/modals.d.ts +7 -0
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/modals.js +494 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -0
- package/dist/homebridge-ui/public/js/modals.ts +546 -0
- package/dist/homebridge-ui/public/js/render.d.ts +68 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/render.js +914 -0
- package/dist/homebridge-ui/public/js/render.js.map +1 -0
- package/dist/homebridge-ui/public/js/render.ts +1026 -0
- package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
- package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/toast.js +29 -0
- package/dist/homebridge-ui/public/js/toast.js.map +1 -0
- package/dist/homebridge-ui/public/js/toast.ts +37 -0
- package/dist/homebridge-ui/public/js/types.d.ts +23 -0
- package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/types.js +2 -0
- package/dist/homebridge-ui/public/js/types.js.map +1 -0
- package/dist/homebridge-ui/public/js/types.ts +26 -0
- package/dist/homebridge-ui/server.js +11 -41
- package/dist/homebridge-ui/server.js.map +1 -1
- package/dist/homebridge-ui/utils/config-parser.d.ts +35 -0
- package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/config-parser.js +87 -0
- package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
- package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
- package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/device-migration.js +111 -0
- package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
- package/dist/homebridge-ui/utils/logger.d.ts +7 -0
- package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/logger.js +17 -0
- package/dist/homebridge-ui/utils/logger.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/platform.d.ts +39 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +1587 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.d.ts +10 -249
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +5 -30
- package/dist/settings.js.map +1 -1
- package/dist/switchbotClient.d.ts +32 -0
- package/dist/switchbotClient.d.ts.map +1 -0
- package/dist/switchbotClient.js +253 -0
- package/dist/switchbotClient.js.map +1 -0
- package/dist/utils.d.ts +36 -82
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +40 -786
- package/dist/utils.js.map +1 -1
- package/docs/assets/highlight.css +21 -0
- package/docs/assets/icons.js +1 -1
- package/docs/assets/icons.svg +1 -1
- package/docs/assets/main.js +2 -2
- package/docs/assets/style.css +3 -3
- package/docs/index.html +103 -13
- package/docs/variables/default.html +1 -1
- package/eslint.config.js +7 -8
- package/nodemon.json +2 -2
- package/package.json +36 -32
- package/scripts/build-ui.js +36 -0
- package/scripts/e2e/README.md +25 -0
- package/scripts/e2e/curtain-e2e.sh +70 -0
- package/scripts/e2e/fan-e2e.sh +75 -0
- package/scripts/e2e/light-advanced-e2e.sh +97 -0
- package/scripts/e2e/light-e2e.sh +75 -0
- package/scripts/e2e/list-accessories.sh +19 -0
- package/scripts/e2e/lock-e2e.sh +65 -0
- package/scripts/free-dev-ports.mjs +105 -0
- package/scripts/generate-matter-maps.js +60 -0
- package/scripts/run-e2e-local.sh +14 -0
- package/scripts/sync-device-types.mjs +28 -0
- package/src/device-types.ts +261 -0
- package/src/deviceFactory.ts +208 -0
- package/src/devices/deviceBase.ts +141 -0
- package/src/devices/genericDevice.ts +1345 -0
- package/src/errors.ts +43 -0
- package/src/homebridge-ui/endpoints/config.ts +126 -0
- package/src/homebridge-ui/endpoints/credentials.ts +74 -0
- package/src/homebridge-ui/endpoints/devices.ts +464 -0
- package/src/homebridge-ui/endpoints/discovery.ts +233 -0
- package/src/homebridge-ui/public/css/styles.css +447 -0
- package/src/homebridge-ui/public/index.html +144 -248
- package/src/homebridge-ui/public/js/api.ts +279 -0
- package/src/homebridge-ui/public/js/app.ts +23 -0
- package/src/homebridge-ui/public/js/constants.ts +1 -0
- package/src/homebridge-ui/public/js/credentials.ts +100 -0
- package/src/homebridge-ui/public/js/devices-delete.ts +236 -0
- package/src/homebridge-ui/public/js/devices.ts +114 -0
- package/src/homebridge-ui/public/js/discovery.ts +1492 -0
- package/src/homebridge-ui/public/js/logger.ts +11 -0
- package/src/homebridge-ui/public/js/modal.ts +28 -0
- package/src/homebridge-ui/public/js/modals.ts +546 -0
- package/src/homebridge-ui/public/js/render.ts +1026 -0
- package/src/homebridge-ui/public/js/toast.ts +37 -0
- package/src/homebridge-ui/public/js/types.ts +26 -0
- package/src/homebridge-ui/server.ts +11 -43
- package/src/homebridge-ui/utils/config-parser.ts +108 -0
- package/src/homebridge-ui/utils/device-migration.ts +144 -0
- package/src/homebridge-ui/utils/logger.ts +17 -0
- package/src/index.ts +1 -3
- package/src/platform.ts +1601 -0
- package/src/settings.ts +12 -277
- package/src/switchbotClient.ts +251 -0
- package/src/utils.ts +48 -812
- package/test/accessory-restore.spec.ts +73 -0
- package/test/device-mapping.spec.ts +37 -0
- package/test/deviceFactory.spec.ts +18 -0
- package/test/e2e/run-e2e.spec.ts +52 -0
- package/test/fan-swing.spec.ts +29 -0
- package/test/helpers/matter-harness.ts +53 -0
- package/test/lock-users.spec.ts +44 -0
- package/test/matter-childbridge.spec.ts +59 -0
- package/test/matter-descriptors.spec.ts +110 -0
- package/test/matter-device-state.spec.ts +101 -0
- package/test/matter-integration.spec.ts +136 -0
- package/test/platform.integration.spec.ts +55 -0
- package/test/switchbot-client-debounce.spec.ts +131 -0
- package/test/switchbot-client-openapi.spec.ts +56 -0
- package/test/switchbotClient.spec.ts +10 -0
- package/test/utils.spec.ts +36 -0
- package/todo/COMPLETED.md +542 -0
- package/todo/README.md +59 -0
- package/todo/TODO.md +190 -0
- package/todo/mockups/filtering-sorting.svg +112 -0
- package/todo/mockups/modal-switchbot-theme.svg +66 -0
- package/todo/mockups/signal-strength-visualization.svg +94 -0
- package/vitest.config.ts +7 -0
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -15847
- package/coverage/coverage-final.json +0 -42
- package/coverage/docs/assets/dmt/dmt-component-data.js.html +0 -85
- package/coverage/docs/assets/dmt/dmt-components.js.html +0 -286
- package/coverage/docs/assets/dmt/index.html +0 -131
- package/coverage/docs/assets/hierarchy.js.html +0 -85
- package/coverage/docs/assets/icons.js.html +0 -136
- package/coverage/docs/assets/index.html +0 -146
- package/coverage/docs/assets/main.js.html +0 -265
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -191
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -196
- package/coverage/src/device/blindtilt.ts.html +0 -3238
- package/coverage/src/device/bot.ts.html +0 -2803
- package/coverage/src/device/ceilinglight.ts.html +0 -2338
- package/coverage/src/device/colorbulb.ts.html +0 -2824
- package/coverage/src/device/contact.ts.html +0 -1465
- package/coverage/src/device/curtain.ts.html +0 -2869
- package/coverage/src/device/device.ts.html +0 -2500
- package/coverage/src/device/fan.ts.html +0 -2242
- package/coverage/src/device/hub.ts.html +0 -1408
- package/coverage/src/device/humidifier.ts.html +0 -2116
- package/coverage/src/device/index.html +0 -416
- package/coverage/src/device/iosensor.ts.html +0 -1375
- package/coverage/src/device/lightstrip.ts.html +0 -2617
- package/coverage/src/device/lock.ts.html +0 -1963
- package/coverage/src/device/meter.ts.html +0 -1372
- package/coverage/src/device/meterplus.ts.html +0 -1384
- package/coverage/src/device/meterpro.ts.html +0 -1618
- package/coverage/src/device/motion.ts.html +0 -1264
- package/coverage/src/device/plug.ts.html +0 -1372
- package/coverage/src/device/relayswitch.ts.html +0 -2284
- package/coverage/src/device/robotvacuumcleaner.ts.html +0 -1810
- package/coverage/src/device/waterdetector.ts.html +0 -1294
- package/coverage/src/homebridge-ui/index.html +0 -116
- package/coverage/src/homebridge-ui/server.ts.html +0 -229
- package/coverage/src/index.html +0 -161
- package/coverage/src/index.ts.html +0 -124
- package/coverage/src/irdevice/airconditioner.ts.html +0 -1687
- package/coverage/src/irdevice/airpurifier.ts.html +0 -844
- package/coverage/src/irdevice/camera.ts.html +0 -475
- package/coverage/src/irdevice/fan.ts.html +0 -766
- package/coverage/src/irdevice/index.html +0 -251
- package/coverage/src/irdevice/irdevice.ts.html +0 -1117
- package/coverage/src/irdevice/light.ts.html +0 -826
- package/coverage/src/irdevice/other.ts.html +0 -2458
- package/coverage/src/irdevice/tv.ts.html +0 -1222
- package/coverage/src/irdevice/vacuumcleaner.ts.html +0 -466
- package/coverage/src/irdevice/waterheater.ts.html +0 -469
- package/coverage/src/platform.ts.html +0 -8776
- package/coverage/src/settings.ts.html +0 -934
- package/coverage/src/utils.ts.html +0 -2092
- package/dist/baseMatterAccessory.test.d.ts +0 -2
- package/dist/baseMatterAccessory.test.d.ts.map +0 -1
- package/dist/baseMatterAccessory.test.js +0 -71
- package/dist/baseMatterAccessory.test.js.map +0 -1
- package/dist/devices-hap/airpurifier.d.ts +0 -54
- package/dist/devices-hap/airpurifier.d.ts.map +0 -1
- package/dist/devices-hap/airpurifier.js +0 -527
- package/dist/devices-hap/airpurifier.js.map +0 -1
- package/dist/devices-hap/blindtilt.d.ts +0 -90
- package/dist/devices-hap/blindtilt.d.ts.map +0 -1
- package/dist/devices-hap/blindtilt.js +0 -974
- package/dist/devices-hap/blindtilt.js.map +0 -1
- package/dist/devices-hap/bot.d.ts +0 -102
- package/dist/devices-hap/bot.d.ts.map +0 -1
- package/dist/devices-hap/bot.js +0 -811
- package/dist/devices-hap/bot.js.map +0 -1
- package/dist/devices-hap/ceilinglight.d.ts +0 -85
- package/dist/devices-hap/ceilinglight.d.ts.map +0 -1
- package/dist/devices-hap/ceilinglight.js +0 -701
- package/dist/devices-hap/ceilinglight.js.map +0 -1
- package/dist/devices-hap/colorbulb.d.ts +0 -88
- package/dist/devices-hap/colorbulb.d.ts.map +0 -1
- package/dist/devices-hap/colorbulb.js +0 -881
- package/dist/devices-hap/colorbulb.js.map +0 -1
- package/dist/devices-hap/contact.d.ts +0 -44
- package/dist/devices-hap/contact.d.ts.map +0 -1
- package/dist/devices-hap/contact.js +0 -409
- package/dist/devices-hap/contact.js.map +0 -1
- package/dist/devices-hap/curtain.d.ts +0 -73
- package/dist/devices-hap/curtain.d.ts.map +0 -1
- package/dist/devices-hap/curtain.js +0 -869
- package/dist/devices-hap/curtain.js.map +0 -1
- package/dist/devices-hap/device.d.ts +0 -98
- package/dist/devices-hap/device.d.ts.map +0 -1
- package/dist/devices-hap/device.js +0 -749
- package/dist/devices-hap/device.js.map +0 -1
- package/dist/devices-hap/fan.d.ts +0 -69
- package/dist/devices-hap/fan.d.ts.map +0 -1
- package/dist/devices-hap/fan.js +0 -649
- package/dist/devices-hap/fan.js.map +0 -1
- package/dist/devices-hap/hub.d.ts +0 -37
- package/dist/devices-hap/hub.d.ts.map +0 -1
- package/dist/devices-hap/hub.js +0 -392
- package/dist/devices-hap/hub.js.map +0 -1
- package/dist/devices-hap/humidifier.d.ts +0 -68
- package/dist/devices-hap/humidifier.d.ts.map +0 -1
- package/dist/devices-hap/humidifier.js +0 -628
- package/dist/devices-hap/humidifier.js.map +0 -1
- package/dist/devices-hap/iosensor.d.ts +0 -42
- package/dist/devices-hap/iosensor.d.ts.map +0 -1
- package/dist/devices-hap/iosensor.js +0 -382
- package/dist/devices-hap/iosensor.js.map +0 -1
- package/dist/devices-hap/lightstrip.d.ts +0 -79
- package/dist/devices-hap/lightstrip.d.ts.map +0 -1
- package/dist/devices-hap/lightstrip.js +0 -797
- package/dist/devices-hap/lightstrip.js.map +0 -1
- package/dist/devices-hap/lock.d.ts +0 -53
- package/dist/devices-hap/lock.d.ts.map +0 -1
- package/dist/devices-hap/lock.js +0 -561
- package/dist/devices-hap/lock.js.map +0 -1
- package/dist/devices-hap/meter.d.ts +0 -37
- package/dist/devices-hap/meter.d.ts.map +0 -1
- package/dist/devices-hap/meter.js +0 -379
- package/dist/devices-hap/meter.js.map +0 -1
- package/dist/devices-hap/meterplus.d.ts +0 -42
- package/dist/devices-hap/meterplus.d.ts.map +0 -1
- package/dist/devices-hap/meterplus.js +0 -384
- package/dist/devices-hap/meterplus.js.map +0 -1
- package/dist/devices-hap/meterpro.d.ts +0 -43
- package/dist/devices-hap/meterpro.d.ts.map +0 -1
- package/dist/devices-hap/meterpro.js +0 -468
- package/dist/devices-hap/meterpro.js.map +0 -1
- package/dist/devices-hap/motion.d.ts +0 -42
- package/dist/devices-hap/motion.d.ts.map +0 -1
- package/dist/devices-hap/motion.js +0 -345
- package/dist/devices-hap/motion.js.map +0 -1
- package/dist/devices-hap/plug.d.ts +0 -49
- package/dist/devices-hap/plug.d.ts.map +0 -1
- package/dist/devices-hap/plug.js +0 -395
- package/dist/devices-hap/plug.js.map +0 -1
- package/dist/devices-hap/relayswitch.d.ts +0 -96
- package/dist/devices-hap/relayswitch.d.ts.map +0 -1
- package/dist/devices-hap/relayswitch.js +0 -642
- package/dist/devices-hap/relayswitch.js.map +0 -1
- package/dist/devices-hap/robotvacuumcleaner.d.ts +0 -54
- package/dist/devices-hap/robotvacuumcleaner.d.ts.map +0 -1
- package/dist/devices-hap/robotvacuumcleaner.js +0 -523
- package/dist/devices-hap/robotvacuumcleaner.js.map +0 -1
- package/dist/devices-hap/waterdetector.d.ts +0 -41
- package/dist/devices-hap/waterdetector.d.ts.map +0 -1
- package/dist/devices-hap/waterdetector.js +0 -356
- package/dist/devices-hap/waterdetector.js.map +0 -1
- package/dist/devices-matter/BaseMatterAccessory.d.ts +0 -78
- package/dist/devices-matter/BaseMatterAccessory.d.ts.map +0 -1
- package/dist/devices-matter/BaseMatterAccessory.js +0 -220
- package/dist/devices-matter/BaseMatterAccessory.js.map +0 -1
- package/dist/devices-matter/ColorLightAccessory.d.ts +0 -20
- package/dist/devices-matter/ColorLightAccessory.d.ts.map +0 -1
- package/dist/devices-matter/ColorLightAccessory.js +0 -95
- package/dist/devices-matter/ColorLightAccessory.js.map +0 -1
- package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts +0 -18
- package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts.map +0 -1
- package/dist/devices-matter/ColorTemperatureLightAccessory.js +0 -76
- package/dist/devices-matter/ColorTemperatureLightAccessory.js.map +0 -1
- package/dist/devices-matter/ContactSensorAccessory.d.ts +0 -12
- package/dist/devices-matter/ContactSensorAccessory.d.ts.map +0 -1
- package/dist/devices-matter/ContactSensorAccessory.js +0 -34
- package/dist/devices-matter/ContactSensorAccessory.js.map +0 -1
- package/dist/devices-matter/DimmableLightAccessory.d.ts +0 -58
- package/dist/devices-matter/DimmableLightAccessory.d.ts.map +0 -1
- package/dist/devices-matter/DimmableLightAccessory.js +0 -167
- package/dist/devices-matter/DimmableLightAccessory.js.map +0 -1
- package/dist/devices-matter/DoorLockAccessory.d.ts +0 -14
- package/dist/devices-matter/DoorLockAccessory.d.ts.map +0 -1
- package/dist/devices-matter/DoorLockAccessory.js +0 -50
- package/dist/devices-matter/DoorLockAccessory.js.map +0 -1
- package/dist/devices-matter/ExtendedColorLightAccessory.d.ts +0 -21
- package/dist/devices-matter/ExtendedColorLightAccessory.d.ts.map +0 -1
- package/dist/devices-matter/ExtendedColorLightAccessory.js +0 -106
- package/dist/devices-matter/ExtendedColorLightAccessory.js.map +0 -1
- package/dist/devices-matter/FanAccessory.d.ts +0 -16
- package/dist/devices-matter/FanAccessory.d.ts.map +0 -1
- package/dist/devices-matter/FanAccessory.js +0 -81
- package/dist/devices-matter/FanAccessory.js.map +0 -1
- package/dist/devices-matter/HumiditySensorAccessory.d.ts +0 -12
- package/dist/devices-matter/HumiditySensorAccessory.d.ts.map +0 -1
- package/dist/devices-matter/HumiditySensorAccessory.js +0 -34
- package/dist/devices-matter/HumiditySensorAccessory.js.map +0 -1
- package/dist/devices-matter/LeakSensorAccessory.d.ts +0 -12
- package/dist/devices-matter/LeakSensorAccessory.d.ts.map +0 -1
- package/dist/devices-matter/LeakSensorAccessory.js +0 -33
- package/dist/devices-matter/LeakSensorAccessory.js.map +0 -1
- package/dist/devices-matter/LightSensorAccessory.d.ts +0 -12
- package/dist/devices-matter/LightSensorAccessory.d.ts.map +0 -1
- package/dist/devices-matter/LightSensorAccessory.js +0 -34
- package/dist/devices-matter/LightSensorAccessory.js.map +0 -1
- package/dist/devices-matter/OccupancySensorAccessory.d.ts +0 -12
- package/dist/devices-matter/OccupancySensorAccessory.d.ts.map +0 -1
- package/dist/devices-matter/OccupancySensorAccessory.js +0 -39
- package/dist/devices-matter/OccupancySensorAccessory.js.map +0 -1
- package/dist/devices-matter/OnOffLightAccessory.d.ts +0 -38
- package/dist/devices-matter/OnOffLightAccessory.d.ts.map +0 -1
- package/dist/devices-matter/OnOffLightAccessory.js +0 -110
- package/dist/devices-matter/OnOffLightAccessory.js.map +0 -1
- package/dist/devices-matter/OnOffOutletAccessory.d.ts +0 -14
- package/dist/devices-matter/OnOffOutletAccessory.d.ts.map +0 -1
- package/dist/devices-matter/OnOffOutletAccessory.js +0 -43
- package/dist/devices-matter/OnOffOutletAccessory.js.map +0 -1
- package/dist/devices-matter/OnOffSwitchAccessory.d.ts +0 -14
- package/dist/devices-matter/OnOffSwitchAccessory.d.ts.map +0 -1
- package/dist/devices-matter/OnOffSwitchAccessory.js +0 -42
- package/dist/devices-matter/OnOffSwitchAccessory.js.map +0 -1
- package/dist/devices-matter/RoboticVacuumAccessory.d.ts +0 -68
- package/dist/devices-matter/RoboticVacuumAccessory.d.ts.map +0 -1
- package/dist/devices-matter/RoboticVacuumAccessory.js +0 -334
- package/dist/devices-matter/RoboticVacuumAccessory.js.map +0 -1
- package/dist/devices-matter/SmokeCOAlarmAccessory.d.ts +0 -11
- package/dist/devices-matter/SmokeCOAlarmAccessory.d.ts.map +0 -1
- package/dist/devices-matter/SmokeCOAlarmAccessory.js +0 -49
- package/dist/devices-matter/SmokeCOAlarmAccessory.js.map +0 -1
- package/dist/devices-matter/TemperatureSensorAccessory.d.ts +0 -12
- package/dist/devices-matter/TemperatureSensorAccessory.d.ts.map +0 -1
- package/dist/devices-matter/TemperatureSensorAccessory.js +0 -36
- package/dist/devices-matter/TemperatureSensorAccessory.js.map +0 -1
- package/dist/devices-matter/ThermostatAccessory.d.ts +0 -19
- package/dist/devices-matter/ThermostatAccessory.d.ts.map +0 -1
- package/dist/devices-matter/ThermostatAccessory.js +0 -95
- package/dist/devices-matter/ThermostatAccessory.js.map +0 -1
- package/dist/devices-matter/VenetianBlindAccessory.d.ts +0 -19
- package/dist/devices-matter/VenetianBlindAccessory.d.ts.map +0 -1
- package/dist/devices-matter/VenetianBlindAccessory.js +0 -99
- package/dist/devices-matter/VenetianBlindAccessory.js.map +0 -1
- package/dist/devices-matter/WindowBlindAccessory.d.ts +0 -17
- package/dist/devices-matter/WindowBlindAccessory.d.ts.map +0 -1
- package/dist/devices-matter/WindowBlindAccessory.js +0 -80
- package/dist/devices-matter/WindowBlindAccessory.js.map +0 -1
- package/dist/devices-matter/custom/PowerStripAccessory.d.ts +0 -97
- package/dist/devices-matter/custom/PowerStripAccessory.d.ts.map +0 -1
- package/dist/devices-matter/custom/PowerStripAccessory.js +0 -265
- package/dist/devices-matter/custom/PowerStripAccessory.js.map +0 -1
- package/dist/devices-matter/custom/index.d.ts +0 -8
- package/dist/devices-matter/custom/index.d.ts.map +0 -1
- package/dist/devices-matter/custom/index.js +0 -8
- package/dist/devices-matter/custom/index.js.map +0 -1
- package/dist/devices-matter/index.d.ts +0 -29
- package/dist/devices-matter/index.d.ts.map +0 -1
- package/dist/devices-matter/index.js +0 -28
- package/dist/devices-matter/index.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -19
- package/dist/index.test.js.map +0 -1
- package/dist/irdevice/airconditioner.d.ts +0 -61
- package/dist/irdevice/airconditioner.d.ts.map +0 -1
- package/dist/irdevice/airconditioner.js +0 -472
- package/dist/irdevice/airconditioner.js.map +0 -1
- package/dist/irdevice/airpurifier.d.ts +0 -50
- package/dist/irdevice/airpurifier.d.ts.map +0 -1
- package/dist/irdevice/airpurifier.js +0 -213
- package/dist/irdevice/airpurifier.js.map +0 -1
- package/dist/irdevice/camera.d.ts +0 -32
- package/dist/irdevice/camera.d.ts.map +0 -1
- package/dist/irdevice/camera.js +0 -107
- package/dist/irdevice/camera.js.map +0 -1
- package/dist/irdevice/fan.d.ts +0 -36
- package/dist/irdevice/fan.d.ts.map +0 -1
- package/dist/irdevice/fan.js +0 -200
- package/dist/irdevice/fan.js.map +0 -1
- package/dist/irdevice/irdevice.d.ts +0 -68
- package/dist/irdevice/irdevice.d.ts.map +0 -1
- package/dist/irdevice/irdevice.js +0 -298
- package/dist/irdevice/irdevice.js.map +0 -1
- package/dist/irdevice/light.d.ts +0 -36
- package/dist/irdevice/light.d.ts.map +0 -1
- package/dist/irdevice/light.js +0 -206
- package/dist/irdevice/light.js.map +0 -1
- package/dist/irdevice/other.d.ts +0 -57
- package/dist/irdevice/other.d.ts.map +0 -1
- package/dist/irdevice/other.js +0 -778
- package/dist/irdevice/other.js.map +0 -1
- package/dist/irdevice/tv.d.ts +0 -45
- package/dist/irdevice/tv.d.ts.map +0 -1
- package/dist/irdevice/tv.js +0 -327
- package/dist/irdevice/tv.js.map +0 -1
- package/dist/irdevice/vacuumcleaner.d.ts +0 -28
- package/dist/irdevice/vacuumcleaner.d.ts.map +0 -1
- package/dist/irdevice/vacuumcleaner.js +0 -104
- package/dist/irdevice/vacuumcleaner.js.map +0 -1
- package/dist/irdevice/waterheater.d.ts +0 -30
- package/dist/irdevice/waterheater.d.ts.map +0 -1
- package/dist/irdevice/waterheater.js +0 -105
- package/dist/irdevice/waterheater.js.map +0 -1
- package/dist/platform-hap.d.ts +0 -149
- package/dist/platform-hap.d.ts.map +0 -1
- package/dist/platform-hap.js +0 -2861
- package/dist/platform-hap.js.map +0 -1
- package/dist/platform-matter.d.ts +0 -120
- package/dist/platform-matter.d.ts.map +0 -1
- package/dist/platform-matter.js +0 -999
- package/dist/platform-matter.js.map +0 -1
- package/dist/utils.test.d.ts +0 -2
- package/dist/utils.test.d.ts.map +0 -1
- package/dist/utils.test.js +0 -95
- package/dist/utils.test.js.map +0 -1
- package/dist/verifyconfig.test.d.ts +0 -2
- package/dist/verifyconfig.test.d.ts.map +0 -1
- package/dist/verifyconfig.test.js +0 -167
- package/dist/verifyconfig.test.js.map +0 -1
- package/src/baseMatterAccessory.test.ts +0 -88
- package/src/custom.d.ts +0 -7
- package/src/devices-hap/airpurifier.ts +0 -563
- package/src/devices-hap/blindtilt.ts +0 -1049
- package/src/devices-hap/bot.ts +0 -900
- package/src/devices-hap/ceilinglight.ts +0 -742
- package/src/devices-hap/colorbulb.ts +0 -904
- package/src/devices-hap/contact.ts +0 -457
- package/src/devices-hap/curtain.ts +0 -944
- package/src/devices-hap/device.ts +0 -811
- package/src/devices-hap/fan.ts +0 -711
- package/src/devices-hap/hub.ts +0 -439
- package/src/devices-hap/humidifier.ts +0 -669
- package/src/devices-hap/iosensor.ts +0 -427
- package/src/devices-hap/lightstrip.ts +0 -836
- package/src/devices-hap/lock.ts +0 -620
- package/src/devices-hap/meter.ts +0 -426
- package/src/devices-hap/meterplus.ts +0 -430
- package/src/devices-hap/meterpro.ts +0 -522
- package/src/devices-hap/motion.ts +0 -390
- package/src/devices-hap/plug.ts +0 -423
- package/src/devices-hap/relayswitch.ts +0 -727
- package/src/devices-hap/robotvacuumcleaner.ts +0 -568
- package/src/devices-hap/waterdetector.ts +0 -400
- package/src/devices-matter/BaseMatterAccessory.ts +0 -249
- package/src/devices-matter/ColorLightAccessory.ts +0 -110
- package/src/devices-matter/ColorTemperatureLightAccessory.ts +0 -90
- package/src/devices-matter/ContactSensorAccessory.ts +0 -41
- package/src/devices-matter/DimmableLightAccessory.ts +0 -192
- package/src/devices-matter/DoorLockAccessory.ts +0 -60
- package/src/devices-matter/ExtendedColorLightAccessory.ts +0 -122
- package/src/devices-matter/FanAccessory.ts +0 -95
- package/src/devices-matter/HumiditySensorAccessory.ts +0 -41
- package/src/devices-matter/LeakSensorAccessory.ts +0 -40
- package/src/devices-matter/LightSensorAccessory.ts +0 -41
- package/src/devices-matter/OccupancySensorAccessory.ts +0 -48
- package/src/devices-matter/OnOffLightAccessory.ts +0 -125
- package/src/devices-matter/OnOffOutletAccessory.ts +0 -51
- package/src/devices-matter/OnOffSwitchAccessory.ts +0 -51
- package/src/devices-matter/RoboticVacuumAccessory.ts +0 -407
- package/src/devices-matter/SmokeCOAlarmAccessory.ts +0 -59
- package/src/devices-matter/TemperatureSensorAccessory.ts +0 -43
- package/src/devices-matter/ThermostatAccessory.ts +0 -110
- package/src/devices-matter/VenetianBlindAccessory.ts +0 -115
- package/src/devices-matter/WindowBlindAccessory.ts +0 -92
- package/src/devices-matter/custom/PowerStripAccessory.ts +0 -309
- package/src/devices-matter/custom/index.ts +0 -8
- package/src/devices-matter/index.ts +0 -29
- package/src/index.test.ts +0 -24
- package/src/irdevice/airconditioner.ts +0 -533
- package/src/irdevice/airpurifier.ts +0 -252
- package/src/irdevice/camera.ts +0 -129
- package/src/irdevice/fan.ts +0 -226
- package/src/irdevice/irdevice.ts +0 -344
- package/src/irdevice/light.ts +0 -246
- package/src/irdevice/other.ts +0 -790
- package/src/irdevice/tv.ts +0 -378
- package/src/irdevice/vacuumcleaner.ts +0 -126
- package/src/irdevice/waterheater.ts +0 -127
- package/src/platform-hap.ts +0 -2997
- package/src/platform-matter.ts +0 -1124
- package/src/utils.test.ts +0 -96
- package/src/verifyconfig.test.ts +0 -197
package/src/platform.ts
ADDED
|
@@ -0,0 +1,1601 @@
|
|
|
1
|
+
import type { SwitchBotPluginConfig } from './settings.js'
|
|
2
|
+
import type { API, Logger, PlatformConfig } from 'homebridge'
|
|
3
|
+
|
|
4
|
+
import { createDevice } from './deviceFactory.js'
|
|
5
|
+
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'
|
|
6
|
+
import { SwitchBotClient } from './switchbotClient.js'
|
|
7
|
+
|
|
8
|
+
// Which device types should prefer Matter if available
|
|
9
|
+
// Based on HAP service mappings: device implementations use specific HomeKit services
|
|
10
|
+
// that map to corresponding Matter clusters when Matter is enabled
|
|
11
|
+
const DEVICE_MATTER_SUPPORTED: Record<string, boolean> = {
|
|
12
|
+
// Core devices
|
|
13
|
+
'bot': true, // Switch → OnOff
|
|
14
|
+
'curtain': true, // WindowCovering → WindowCovering
|
|
15
|
+
'fan': true, // Fan → FanControl
|
|
16
|
+
'light': true, // Lightbulb → OnOff + LevelControl
|
|
17
|
+
'lightstrip': true, // Lightbulb (color) → OnOff + LevelControl + ColorControl
|
|
18
|
+
'motion': true, // MotionSensor → OccupancySensing
|
|
19
|
+
'contact': true, // ContactSensor → BooleanState
|
|
20
|
+
'vacuum': true, // Switch → RobotVacuumCleaner
|
|
21
|
+
'lock': true, // LockMechanism → DoorLock
|
|
22
|
+
'humidifier': true, // Fan + Humidity → OnOff + FanControl + RelativeHumidityMeasurement
|
|
23
|
+
'temperature': true, // TemperatureSensor → TemperatureMeasurement
|
|
24
|
+
|
|
25
|
+
// Switch devices
|
|
26
|
+
'relay': true, // Switch → OnOff
|
|
27
|
+
'relay switch 1': true, // Switch → OnOff
|
|
28
|
+
'relay switch 1pm': true, // Switch → OnOff
|
|
29
|
+
'plug': true, // Outlet → OnOff
|
|
30
|
+
'plug mini (jp)': true, // Outlet → OnOff
|
|
31
|
+
'plug mini (us)': true, // Outlet → OnOff
|
|
32
|
+
|
|
33
|
+
// Window covering variants
|
|
34
|
+
'blindtilt': true, // WindowCovering → WindowCovering
|
|
35
|
+
'blind tilt': true, // WindowCovering → WindowCovering
|
|
36
|
+
'curtain3': true, // WindowCovering → WindowCovering
|
|
37
|
+
'rollershade': true, // WindowCovering → WindowCovering
|
|
38
|
+
'roller shade': true, // WindowCovering → WindowCovering
|
|
39
|
+
'worollershade': true, // WindowCovering → WindowCovering
|
|
40
|
+
'wo rollershade': true, // WindowCovering → WindowCovering
|
|
41
|
+
|
|
42
|
+
// Vacuum variants (normalized to 'vacuum' before lookup)
|
|
43
|
+
'wosweeper': true, // VacuumDevice → RobotVacuumCleaner
|
|
44
|
+
'wosweepermini': true, // VacuumDevice → RobotVacuumCleaner
|
|
45
|
+
'wosweeperminipro': true, // VacuumDevice → RobotVacuumCleaner
|
|
46
|
+
'k10+': true, // VacuumDevice → RobotVacuumCleaner
|
|
47
|
+
'k10+ pro': true, // VacuumDevice → RobotVacuumCleaner
|
|
48
|
+
|
|
49
|
+
// Sensors
|
|
50
|
+
'meter': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
|
|
51
|
+
'meterplus': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
|
|
52
|
+
'meter plus (jp)': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
|
|
53
|
+
'meterpro': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
|
|
54
|
+
'meterpro(co2)': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
|
|
55
|
+
'waterdetector': true, // LeakSensor → BooleanState
|
|
56
|
+
'water detector': true, // LeakSensor → BooleanState
|
|
57
|
+
|
|
58
|
+
// Other devices
|
|
59
|
+
'smart fan': true, // Fan → FanControl
|
|
60
|
+
'strip light': true, // Lightbulb (color) → OnOff + LevelControl + ColorControl
|
|
61
|
+
'hub 2': false, // Hub device - not exposed as accessory
|
|
62
|
+
'walletfinder': false, // Button device - Matter support TBD
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Default Matter cluster configurations by device type
|
|
66
|
+
// Maps device types to their Matter cluster states (used when device doesn't provide clusters)
|
|
67
|
+
// Note: wosweeper/curtain/plug variants are normalized before cluster lookup (see loadDevices)
|
|
68
|
+
export const DEVICE_MATTER_CLUSTERS: Record<string, any> = {
|
|
69
|
+
// Core devices - aligned with HAP service implementations
|
|
70
|
+
bot: { onOff: { onOff: false } }, // Switch → OnOff
|
|
71
|
+
vacuum: {
|
|
72
|
+
rvcRunMode: {
|
|
73
|
+
supportedModes: [
|
|
74
|
+
{ label: 'Idle', mode: 0, modeTags: [{ value: 16384 }] },
|
|
75
|
+
{ label: 'Cleaning', mode: 1, modeTags: [{ value: 16385 }] },
|
|
76
|
+
],
|
|
77
|
+
currentMode: 0,
|
|
78
|
+
},
|
|
79
|
+
rvcCleanMode: {
|
|
80
|
+
supportedModes: [
|
|
81
|
+
{ label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
|
|
82
|
+
],
|
|
83
|
+
currentMode: 0,
|
|
84
|
+
},
|
|
85
|
+
rvcOperationalState: {
|
|
86
|
+
operationalStateList: [
|
|
87
|
+
{ operationalStateId: 0 }, // Stopped
|
|
88
|
+
{ operationalStateId: 1 }, // Running
|
|
89
|
+
{ operationalStateId: 2 }, // Paused
|
|
90
|
+
{ operationalStateId: 3 }, // Error (required)
|
|
91
|
+
{ operationalStateId: 64 }, // Seeking charger
|
|
92
|
+
{ operationalStateId: 65 }, // Charging
|
|
93
|
+
{ operationalStateId: 66 }, // Docked
|
|
94
|
+
],
|
|
95
|
+
operationalState: 66,
|
|
96
|
+
},
|
|
97
|
+
}, // Switch in HAP, RobotVacuumCleaner in Matter
|
|
98
|
+
curtain: {
|
|
99
|
+
/* windowCovering: {
|
|
100
|
+
type: 0, // Generic - supports basic open/close/position features, but no tilt (curtain3/rollershade variants use this with normalization)
|
|
101
|
+
}, */
|
|
102
|
+
}, // WindowCovering → WindowCovering (includes curtain3, rollershade variants via normalization)
|
|
103
|
+
blindtilt: {
|
|
104
|
+
windowCovering: {
|
|
105
|
+
type: 8, // TiltBlindLiftAndTilt - supports tilt feature
|
|
106
|
+
},
|
|
107
|
+
}, // WindowCovering with tilt → WindowCovering
|
|
108
|
+
fan: {
|
|
109
|
+
onOff: { onOff: false },
|
|
110
|
+
fanControl: {
|
|
111
|
+
fanMode: 0,
|
|
112
|
+
percentCurrent: 0,
|
|
113
|
+
percentSetting: 0,
|
|
114
|
+
speedCurrent: 0,
|
|
115
|
+
speedMax: 100,
|
|
116
|
+
},
|
|
117
|
+
}, // Fan → OnOff + FanControl
|
|
118
|
+
light: {
|
|
119
|
+
onOff: { onOff: false },
|
|
120
|
+
levelControl: {
|
|
121
|
+
currentLevel: 0,
|
|
122
|
+
minLevel: 0,
|
|
123
|
+
maxLevel: 254,
|
|
124
|
+
},
|
|
125
|
+
}, // Lightbulb → OnOff + LevelControl
|
|
126
|
+
lightstrip: {
|
|
127
|
+
onOff: { onOff: false },
|
|
128
|
+
levelControl: {
|
|
129
|
+
currentLevel: 0,
|
|
130
|
+
minLevel: 0,
|
|
131
|
+
maxLevel: 254,
|
|
132
|
+
},
|
|
133
|
+
colorControl: {
|
|
134
|
+
colorMode: 0,
|
|
135
|
+
},
|
|
136
|
+
}, // Lightbulb with color → OnOff + LevelControl + ColorControl
|
|
137
|
+
lock: {
|
|
138
|
+
doorLock: {
|
|
139
|
+
lockState: 0,
|
|
140
|
+
lockType: 0,
|
|
141
|
+
actuatorEnabled: true,
|
|
142
|
+
operatingMode: 0,
|
|
143
|
+
},
|
|
144
|
+
}, // LockMechanism → DoorLock
|
|
145
|
+
motion: {
|
|
146
|
+
occupancySensing: {
|
|
147
|
+
occupancy: 0,
|
|
148
|
+
occupancySensorType: 0,
|
|
149
|
+
},
|
|
150
|
+
}, // MotionSensor → OccupancySensing
|
|
151
|
+
contact: {
|
|
152
|
+
booleanState: {
|
|
153
|
+
stateValue: false,
|
|
154
|
+
},
|
|
155
|
+
}, // ContactSensor → BooleanState
|
|
156
|
+
humidifier: {
|
|
157
|
+
onOff: { onOff: false },
|
|
158
|
+
fanControl: {
|
|
159
|
+
fanMode: 0,
|
|
160
|
+
percentCurrent: 0,
|
|
161
|
+
},
|
|
162
|
+
relativeHumidityMeasurement: {
|
|
163
|
+
measuredValue: 0,
|
|
164
|
+
minMeasuredValue: 0,
|
|
165
|
+
maxMeasuredValue: 100,
|
|
166
|
+
},
|
|
167
|
+
}, // HumidifierDehumidifier → OnOff + FanControl + RelativeHumidityMeasurement
|
|
168
|
+
temperature: {
|
|
169
|
+
temperatureMeasurement: {
|
|
170
|
+
measuredValue: 0,
|
|
171
|
+
minMeasuredValue: -27315,
|
|
172
|
+
maxMeasuredValue: 32767,
|
|
173
|
+
},
|
|
174
|
+
}, // TemperatureSensor → TemperatureMeasurement
|
|
175
|
+
|
|
176
|
+
// Switch/Outlet devices
|
|
177
|
+
relay: { onOff: { onOff: false } }, // Switch → OnOff
|
|
178
|
+
plug: {
|
|
179
|
+
onOff: { onOff: false },
|
|
180
|
+
electricalMeasurement: {
|
|
181
|
+
activePower: 0,
|
|
182
|
+
rmsCurrent: 0,
|
|
183
|
+
rmsVoltage: 0,
|
|
184
|
+
},
|
|
185
|
+
}, // Outlet → OnOff + ElectricalMeasurement (for PM models)
|
|
186
|
+
|
|
187
|
+
// Sensors
|
|
188
|
+
meter: {
|
|
189
|
+
temperatureMeasurement: {
|
|
190
|
+
measuredValue: 0,
|
|
191
|
+
minMeasuredValue: -27315,
|
|
192
|
+
maxMeasuredValue: 32767,
|
|
193
|
+
},
|
|
194
|
+
relativeHumidityMeasurement: {
|
|
195
|
+
measuredValue: 0,
|
|
196
|
+
minMeasuredValue: 0,
|
|
197
|
+
maxMeasuredValue: 100,
|
|
198
|
+
},
|
|
199
|
+
}, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
|
|
200
|
+
waterdetector: {
|
|
201
|
+
booleanState: {
|
|
202
|
+
stateValue: false,
|
|
203
|
+
},
|
|
204
|
+
}, // LeakSensor → BooleanState
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const DEVICE_MATTER_DEVICE_TYPE_KEYS: Record<string, string> = {
|
|
208
|
+
bot: 'OnOffSwitch',
|
|
209
|
+
vacuum: 'RoboticVacuumCleaner',
|
|
210
|
+
curtain: 'WindowCovering',
|
|
211
|
+
blindtilt: 'WindowCovering',
|
|
212
|
+
fan: 'Fan',
|
|
213
|
+
light: 'DimmableLight',
|
|
214
|
+
lightstrip: 'ExtendedColorLight',
|
|
215
|
+
lock: 'DoorLock',
|
|
216
|
+
motion: 'MotionSensor',
|
|
217
|
+
contact: 'ContactSensor',
|
|
218
|
+
humidifier: 'Fan',
|
|
219
|
+
temperature: 'TemperatureSensor',
|
|
220
|
+
relay: 'OnOffSwitch',
|
|
221
|
+
plug: 'OnOffOutlet',
|
|
222
|
+
meter: 'TemperatureSensor',
|
|
223
|
+
waterdetector: 'LeakSensor',
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizeTypeForMatter(typeValue: string | undefined | null): string {
|
|
227
|
+
const raw = String(typeValue || '').trim().toLowerCase()
|
|
228
|
+
if (!raw) {
|
|
229
|
+
return 'unknown'
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Vacuum variants
|
|
233
|
+
if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes(raw)) {
|
|
234
|
+
return 'vacuum'
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Window covering variants
|
|
238
|
+
if (['curtain', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes(raw)) {
|
|
239
|
+
return 'curtain'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Blind tilt variants (normalized to 'blindtilt' for Matter since it uses tilt-capable cluster)
|
|
243
|
+
if (['blindtilt', 'blind tilt'].includes(raw)) {
|
|
244
|
+
return 'blindtilt'
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Plug variants
|
|
248
|
+
if (['plug mini (jp)', 'plug mini (us)', 'plug mini (eu)'].includes(raw)) {
|
|
249
|
+
return 'plug'
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Meter variants
|
|
253
|
+
if (['meterplus', 'meter plus', 'meter plus (jp)', 'meterpro', 'meter pro', 'meterpro(co2)', 'meter pro (co2)'].includes(raw)) {
|
|
254
|
+
return 'meter'
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Relay switch variants
|
|
258
|
+
if (['relay switch 1', 'relay switch 1pm'].includes(raw)) {
|
|
259
|
+
return 'relay'
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Water detector variants
|
|
263
|
+
if (['water detector', 'waterdetector'].includes(raw)) {
|
|
264
|
+
return 'waterdetector'
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Fan variants
|
|
268
|
+
if (['smart fan', 'circulator fan', 'battery circulator fan', 'standing circulator fan'].includes(raw)) {
|
|
269
|
+
return 'fan'
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Light variants
|
|
273
|
+
if (['strip light', 'strip light 3', 'rgbic neon rope light', 'rgbic neon wire rope light', 'rgbicww floor lamp', 'rgbicww strip light'].includes(raw)) {
|
|
274
|
+
return 'lightstrip'
|
|
275
|
+
}
|
|
276
|
+
if (['color bulb', 'ceiling light', 'ceiling light pro', 'candle warmer lamp', 'floor lamp'].includes(raw)) {
|
|
277
|
+
return 'light'
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Sensor variants
|
|
281
|
+
if (raw === 'motion sensor') {
|
|
282
|
+
return 'motion'
|
|
283
|
+
}
|
|
284
|
+
if (['contact sensor', 'presence sensor'].includes(raw)) {
|
|
285
|
+
return 'contact'
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Lock variants
|
|
289
|
+
if (['smart lock', 'smart lock pro', 'smart lock ultra', 'lock lite', 'keypad', 'keypad touch', 'keypad vision', 'keypad vision pro', 'lock vision pro'].includes(raw)) {
|
|
290
|
+
return 'lock'
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Climate variant
|
|
294
|
+
if (raw === 'humidifier2') {
|
|
295
|
+
return 'humidifier'
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return raw
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Factory function to create Matter handlers with Homebridge logger integration
|
|
302
|
+
function createMatterHandlers(log: Logger, deviceId: string, type: string, client: any): any {
|
|
303
|
+
const lowerType = type.toLowerCase()
|
|
304
|
+
|
|
305
|
+
switch (lowerType) {
|
|
306
|
+
case 'vacuum':
|
|
307
|
+
return {
|
|
308
|
+
rvcRunMode: {
|
|
309
|
+
changeToMode: async (request: any) => {
|
|
310
|
+
const modeNames = ['Idle', 'Cleaning', 'Mapping']
|
|
311
|
+
const modeName = modeNames[request?.newMode] || `Unknown (${request?.newMode})`
|
|
312
|
+
log.info(`[${deviceId}] RVC run mode change requested: ${modeName}`)
|
|
313
|
+
if (!client) {
|
|
314
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
315
|
+
return { success: false }
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
// For K10+ family: use 'start' to begin cleaning (mode 1 = Cleaning)
|
|
319
|
+
// For older K10+: only supports start/stop/dock
|
|
320
|
+
// For newer K20+/S10/S20: supports startClean with more parameters
|
|
321
|
+
// Map Matter mode to SwitchBot command
|
|
322
|
+
const switchBotCommand = request?.newMode === 1 ? 'start' : 'stop'
|
|
323
|
+
const body = {
|
|
324
|
+
command: switchBotCommand,
|
|
325
|
+
parameter: 'default',
|
|
326
|
+
commandType: 'command',
|
|
327
|
+
}
|
|
328
|
+
log.debug(`[${deviceId}] Sending RVC mode change request:`, JSON.stringify(body))
|
|
329
|
+
const result = await client.setDeviceState(deviceId, body)
|
|
330
|
+
log.debug(`[${deviceId}] RVC mode change API response:`, JSON.stringify(result))
|
|
331
|
+
log.info(`[${deviceId}] RVC mode changed successfully to ${switchBotCommand}`)
|
|
332
|
+
return { success: true, result }
|
|
333
|
+
} catch (e) {
|
|
334
|
+
log.error(`[${deviceId}] Failed to change RVC mode:`, e)
|
|
335
|
+
return { success: false, error: e }
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
rvcCleanMode: {
|
|
340
|
+
changeToMode: async (request: any) => {
|
|
341
|
+
const modeName = request?.newMode !== undefined ? `Mode ${request.newMode}` : 'Unknown'
|
|
342
|
+
log.info(`[${deviceId}] RVC clean mode change requested: ${modeName}`)
|
|
343
|
+
if (!client) {
|
|
344
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
345
|
+
return { success: false }
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
// Clean mode (vacuum/mop/etc) not directly supported via Matter for K10+
|
|
349
|
+
// K20+ Pro and newer models support via startClean action parameter
|
|
350
|
+
log.info(`[${deviceId}] Clean mode change requires startClean command (not yet implemented for Matter)`)
|
|
351
|
+
return { success: true }
|
|
352
|
+
} catch (e) {
|
|
353
|
+
log.error(`[${deviceId}] Failed to change RVC clean mode:`, e)
|
|
354
|
+
return { success: false, error: e }
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
rvcOperationalState: {
|
|
359
|
+
pause: async () => {
|
|
360
|
+
log.info(`[${deviceId}] RVC pause command received`)
|
|
361
|
+
if (!client) {
|
|
362
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
363
|
+
return { success: false }
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const body = {
|
|
367
|
+
command: 'stop',
|
|
368
|
+
parameter: 'default',
|
|
369
|
+
commandType: 'command',
|
|
370
|
+
}
|
|
371
|
+
log.debug(`[${deviceId}] Sending RVC pause request:`, JSON.stringify(body))
|
|
372
|
+
const result = await client.setDeviceState(deviceId, body)
|
|
373
|
+
log.debug(`[${deviceId}] RVC pause API response:`, JSON.stringify(result))
|
|
374
|
+
log.info(`[${deviceId}] RVC paused successfully`)
|
|
375
|
+
return { success: true, result }
|
|
376
|
+
} catch (e) {
|
|
377
|
+
log.error(`[${deviceId}] Failed to pause RVC:`, e)
|
|
378
|
+
return { success: false, error: e }
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
resume: async () => {
|
|
382
|
+
log.info(`[${deviceId}] RVC resume command received`)
|
|
383
|
+
if (!client) {
|
|
384
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
385
|
+
return { success: false }
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
const body = {
|
|
389
|
+
command: 'start',
|
|
390
|
+
parameter: 'default',
|
|
391
|
+
commandType: 'command',
|
|
392
|
+
}
|
|
393
|
+
log.debug(`[${deviceId}] Sending RVC resume request:`, JSON.stringify(body))
|
|
394
|
+
const result = await client.setDeviceState(deviceId, body)
|
|
395
|
+
log.debug(`[${deviceId}] RVC resume API response:`, JSON.stringify(result))
|
|
396
|
+
log.info(`[${deviceId}] RVC resumed successfully`)
|
|
397
|
+
return { success: true, result }
|
|
398
|
+
} catch (e) {
|
|
399
|
+
log.error(`[${deviceId}] Failed to resume RVC:`, e)
|
|
400
|
+
return { success: false, error: e }
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
goHome: async () => {
|
|
404
|
+
log.info(`[${deviceId}] RVC goHome command received`)
|
|
405
|
+
if (!client) {
|
|
406
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
407
|
+
return { success: false }
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const body = {
|
|
411
|
+
command: 'dock',
|
|
412
|
+
parameter: 'default',
|
|
413
|
+
commandType: 'command',
|
|
414
|
+
}
|
|
415
|
+
log.debug(`[${deviceId}] Sending RVC goHome request:`, JSON.stringify(body))
|
|
416
|
+
const result = await client.setDeviceState(deviceId, body)
|
|
417
|
+
log.debug(`[${deviceId}] RVC goHome API response:`, JSON.stringify(result))
|
|
418
|
+
log.info(`[${deviceId}] RVC sent to dock successfully`)
|
|
419
|
+
return { success: true, result }
|
|
420
|
+
} catch (e) {
|
|
421
|
+
log.error(`[${deviceId}] Failed to send goHome command:`, e)
|
|
422
|
+
return { success: false, error: e }
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
case 'bot':
|
|
429
|
+
return {
|
|
430
|
+
onOff: {
|
|
431
|
+
on: async () => {
|
|
432
|
+
log.info(`[${deviceId}] Bot ON command received`)
|
|
433
|
+
if (!client) {
|
|
434
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
435
|
+
return { success: false }
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
const result = await client.setDeviceState(deviceId, {
|
|
439
|
+
command: 'turnOn',
|
|
440
|
+
parameter: 'default',
|
|
441
|
+
commandType: 'command',
|
|
442
|
+
})
|
|
443
|
+
log.info(`[${deviceId}] Bot turned on successfully`)
|
|
444
|
+
return { success: true, result }
|
|
445
|
+
} catch (e) {
|
|
446
|
+
log.error(`[${deviceId}] Failed to turn on Bot:`, e)
|
|
447
|
+
return { success: false, error: e }
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
off: async () => {
|
|
451
|
+
log.info(`[${deviceId}] Bot OFF command received`)
|
|
452
|
+
if (!client) {
|
|
453
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
454
|
+
return { success: false }
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const result = await client.setDeviceState(deviceId, {
|
|
458
|
+
command: 'turnOff',
|
|
459
|
+
parameter: 'default',
|
|
460
|
+
commandType: 'command',
|
|
461
|
+
})
|
|
462
|
+
log.info(`[${deviceId}] Bot turned off successfully`)
|
|
463
|
+
return { success: true, result }
|
|
464
|
+
} catch (e) {
|
|
465
|
+
log.error(`[${deviceId}] Failed to turn off Bot:`, e)
|
|
466
|
+
return { success: false, error: e }
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
case 'curtain':
|
|
473
|
+
case 'blindtilt':
|
|
474
|
+
return {
|
|
475
|
+
windowCovering: {
|
|
476
|
+
goToLiftPercentage: async (request: any) => {
|
|
477
|
+
const percentage = request?.liftPercent100thsValue
|
|
478
|
+
log.info(`[${deviceId}] Curtain position change requested: ${percentage}`)
|
|
479
|
+
if (!client) {
|
|
480
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
481
|
+
return { success: false }
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
// Convert Matter percentage (0-10000) to SwitchBot (0-100)
|
|
485
|
+
const position = Math.max(0, Math.min(100, Math.round((percentage || 0) / 100)))
|
|
486
|
+
const result = await client.setDeviceState(deviceId, {
|
|
487
|
+
command: 'setPosition',
|
|
488
|
+
parameter: String(position),
|
|
489
|
+
commandType: 'command',
|
|
490
|
+
})
|
|
491
|
+
log.info(`[${deviceId}] Curtain position set to ${position}% successfully`)
|
|
492
|
+
return { success: true, result }
|
|
493
|
+
} catch (e) {
|
|
494
|
+
log.error(`[${deviceId}] Failed to set curtain position:`, e)
|
|
495
|
+
return { success: false, error: e }
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
upOrOpen: async () => {
|
|
499
|
+
log.info(`[${deviceId}] Curtain open command received`)
|
|
500
|
+
if (!client) {
|
|
501
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
502
|
+
return { success: false }
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const result = await client.setDeviceState(deviceId, {
|
|
506
|
+
command: 'open',
|
|
507
|
+
parameter: 'default',
|
|
508
|
+
commandType: 'command',
|
|
509
|
+
})
|
|
510
|
+
log.info(`[${deviceId}] Curtain opened successfully`)
|
|
511
|
+
return { success: true, result }
|
|
512
|
+
} catch (e) {
|
|
513
|
+
log.error(`[${deviceId}] Failed to open curtain:`, e)
|
|
514
|
+
return { success: false, error: e }
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
downOrClose: async () => {
|
|
518
|
+
log.info(`[${deviceId}] Curtain close command received`)
|
|
519
|
+
if (!client) {
|
|
520
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
521
|
+
return { success: false }
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
const result = await client.setDeviceState(deviceId, {
|
|
525
|
+
command: 'close',
|
|
526
|
+
parameter: 'default',
|
|
527
|
+
commandType: 'command',
|
|
528
|
+
})
|
|
529
|
+
log.info(`[${deviceId}] Curtain closed successfully`)
|
|
530
|
+
return { success: true, result }
|
|
531
|
+
} catch (e) {
|
|
532
|
+
log.error(`[${deviceId}] Failed to close curtain:`, e)
|
|
533
|
+
return { success: false, error: e }
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
stopMotion: async () => {
|
|
537
|
+
log.info(`[${deviceId}] Curtain stop command received`)
|
|
538
|
+
if (!client) {
|
|
539
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
540
|
+
return { success: false }
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
const result = await client.setDeviceState(deviceId, {
|
|
544
|
+
command: 'pause',
|
|
545
|
+
parameter: 'default',
|
|
546
|
+
commandType: 'command',
|
|
547
|
+
})
|
|
548
|
+
log.info(`[${deviceId}] Curtain motion stopped successfully`)
|
|
549
|
+
return { success: true, result }
|
|
550
|
+
} catch (e) {
|
|
551
|
+
log.error(`[${deviceId}] Failed to stop curtain:`, e)
|
|
552
|
+
return { success: false, error: e }
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
case 'plug':
|
|
559
|
+
return {
|
|
560
|
+
onOff: {
|
|
561
|
+
on: async () => {
|
|
562
|
+
log.info(`[${deviceId}] Plug ON command received`)
|
|
563
|
+
if (!client) {
|
|
564
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
565
|
+
return { success: false }
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
const result = await client.setDeviceState(deviceId, {
|
|
569
|
+
command: 'turnOn',
|
|
570
|
+
parameter: 'default',
|
|
571
|
+
commandType: 'command',
|
|
572
|
+
})
|
|
573
|
+
log.info(`[${deviceId}] Plug turned on successfully`)
|
|
574
|
+
return { success: true, result }
|
|
575
|
+
} catch (e) {
|
|
576
|
+
log.error(`[${deviceId}] Failed to turn on plug:`, e)
|
|
577
|
+
return { success: false, error: e }
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
off: async () => {
|
|
581
|
+
log.info(`[${deviceId}] Plug OFF command received`)
|
|
582
|
+
if (!client) {
|
|
583
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
584
|
+
return { success: false }
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const result = await client.setDeviceState(deviceId, {
|
|
588
|
+
command: 'turnOff',
|
|
589
|
+
parameter: 'default',
|
|
590
|
+
commandType: 'command',
|
|
591
|
+
})
|
|
592
|
+
log.info(`[${deviceId}] Plug turned off successfully`)
|
|
593
|
+
return { success: true, result }
|
|
594
|
+
} catch (e) {
|
|
595
|
+
log.error(`[${deviceId}] Failed to turn off plug:`, e)
|
|
596
|
+
return { success: false, error: e }
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
case 'lock':
|
|
603
|
+
return {
|
|
604
|
+
doorLock: {
|
|
605
|
+
setLockState: async (request: any) => {
|
|
606
|
+
const state = request?.lockState === 1 ? 'LOCKED' : 'UNLOCKED'
|
|
607
|
+
log.info(`[${deviceId}] Lock state change requested: ${state}`)
|
|
608
|
+
if (!client) {
|
|
609
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
610
|
+
return { success: false }
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const command = request?.lockState === 1 ? 'lock' : 'unlock'
|
|
614
|
+
const result = await client.setDeviceState(deviceId, {
|
|
615
|
+
command,
|
|
616
|
+
parameter: 'default',
|
|
617
|
+
commandType: 'command',
|
|
618
|
+
})
|
|
619
|
+
log.info(`[${deviceId}] Lock ${state} successfully`)
|
|
620
|
+
return { success: true, result }
|
|
621
|
+
} catch (e) {
|
|
622
|
+
log.error(`[${deviceId}] Failed to change lock state:`, e)
|
|
623
|
+
return { success: false, error: e }
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
case 'fan':
|
|
630
|
+
return {
|
|
631
|
+
onOff: {
|
|
632
|
+
on: async () => {
|
|
633
|
+
log.info(`[${deviceId}] Fan ON command received`)
|
|
634
|
+
if (!client) {
|
|
635
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
636
|
+
return { success: false }
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
const result = await client.setDeviceState(deviceId, {
|
|
640
|
+
command: 'turnOn',
|
|
641
|
+
parameter: 'default',
|
|
642
|
+
commandType: 'command',
|
|
643
|
+
})
|
|
644
|
+
log.info(`[${deviceId}] Fan turned on successfully`)
|
|
645
|
+
return { success: true, result }
|
|
646
|
+
} catch (e) {
|
|
647
|
+
log.error(`[${deviceId}] Failed to turn on fan:`, e)
|
|
648
|
+
return { success: false, error: e }
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
off: async () => {
|
|
652
|
+
log.info(`[${deviceId}] Fan OFF command received`)
|
|
653
|
+
if (!client) {
|
|
654
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
655
|
+
return { success: false }
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
const result = await client.setDeviceState(deviceId, {
|
|
659
|
+
command: 'turnOff',
|
|
660
|
+
parameter: 'default',
|
|
661
|
+
commandType: 'command',
|
|
662
|
+
})
|
|
663
|
+
log.info(`[${deviceId}] Fan turned off successfully`)
|
|
664
|
+
return { success: true, result }
|
|
665
|
+
} catch (e) {
|
|
666
|
+
log.error(`[${deviceId}] Failed to turn off fan:`, e)
|
|
667
|
+
return { success: false, error: e }
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
fanControl: {
|
|
672
|
+
setFanSpeed: async (request: any) => {
|
|
673
|
+
const speed = request?.percentSetting || 0
|
|
674
|
+
log.info(`[${deviceId}] Fan speed change requested: ${speed}%`)
|
|
675
|
+
if (!client) {
|
|
676
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
677
|
+
return { success: false }
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
// Convert percentage to SwitchBot fan speed parameter
|
|
681
|
+
const speedParam = Math.max(1, Math.min(100, speed))
|
|
682
|
+
const result = await client.setDeviceState(deviceId, {
|
|
683
|
+
command: 'setFanSpeed',
|
|
684
|
+
parameter: String(speedParam),
|
|
685
|
+
commandType: 'command',
|
|
686
|
+
})
|
|
687
|
+
log.info(`[${deviceId}] Fan speed set to ${speedParam}% successfully`)
|
|
688
|
+
return { success: true, result }
|
|
689
|
+
} catch (e) {
|
|
690
|
+
log.error(`[${deviceId}] Failed to set fan speed:`, e)
|
|
691
|
+
return { success: false, error: e }
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
case 'light':
|
|
698
|
+
return {
|
|
699
|
+
onOff: {
|
|
700
|
+
on: async () => {
|
|
701
|
+
log.info(`[${deviceId}] Light ON command received`)
|
|
702
|
+
if (!client) {
|
|
703
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
704
|
+
return { success: false }
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
const result = await client.setDeviceState(deviceId, {
|
|
708
|
+
command: 'turnOn',
|
|
709
|
+
parameter: 'default',
|
|
710
|
+
commandType: 'command',
|
|
711
|
+
})
|
|
712
|
+
log.info(`[${deviceId}] Light turned on successfully`)
|
|
713
|
+
return { success: true, result }
|
|
714
|
+
} catch (e) {
|
|
715
|
+
log.error(`[${deviceId}] Failed to turn on light:`, e)
|
|
716
|
+
return { success: false, error: e }
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
off: async () => {
|
|
720
|
+
log.info(`[${deviceId}] Light OFF command received`)
|
|
721
|
+
if (!client) {
|
|
722
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
723
|
+
return { success: false }
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
const result = await client.setDeviceState(deviceId, {
|
|
727
|
+
command: 'turnOff',
|
|
728
|
+
parameter: 'default',
|
|
729
|
+
commandType: 'command',
|
|
730
|
+
})
|
|
731
|
+
log.info(`[${deviceId}] Light turned off successfully`)
|
|
732
|
+
return { success: true, result }
|
|
733
|
+
} catch (e) {
|
|
734
|
+
log.error(`[${deviceId}] Failed to turn off light:`, e)
|
|
735
|
+
return { success: false, error: e }
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
levelControl: {
|
|
740
|
+
moveToLevel: async (request: any) => {
|
|
741
|
+
const level = request?.level || 0
|
|
742
|
+
// Convert from 0-254 to 0-100
|
|
743
|
+
const brightness = Math.round((level / 254) * 100)
|
|
744
|
+
log.info(`[${deviceId}] Light brightness change requested: ${brightness}%`)
|
|
745
|
+
if (!client) {
|
|
746
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
747
|
+
return { success: false }
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
const param = Math.max(0, Math.min(100, brightness))
|
|
751
|
+
const result = await client.setDeviceState(deviceId, {
|
|
752
|
+
command: 'setBrightness',
|
|
753
|
+
parameter: String(param),
|
|
754
|
+
commandType: 'command',
|
|
755
|
+
})
|
|
756
|
+
log.info(`[${deviceId}] Light brightness set to ${param}% successfully`)
|
|
757
|
+
return { success: true, result }
|
|
758
|
+
} catch (e) {
|
|
759
|
+
log.error(`[${deviceId}] Failed to set light brightness:`, e)
|
|
760
|
+
return { success: false, error: e }
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
case 'lightstrip':
|
|
767
|
+
return {
|
|
768
|
+
onOff: {
|
|
769
|
+
on: async () => {
|
|
770
|
+
log.info(`[${deviceId}] Lightstrip ON command received`)
|
|
771
|
+
if (!client) {
|
|
772
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
773
|
+
return { success: false }
|
|
774
|
+
}
|
|
775
|
+
try {
|
|
776
|
+
const result = await client.setDeviceState(deviceId, {
|
|
777
|
+
command: 'turnOn',
|
|
778
|
+
parameter: 'default',
|
|
779
|
+
commandType: 'command',
|
|
780
|
+
})
|
|
781
|
+
log.info(`[${deviceId}] Lightstrip turned on successfully`)
|
|
782
|
+
return { success: true, result }
|
|
783
|
+
} catch (e) {
|
|
784
|
+
log.error(`[${deviceId}] Failed to turn on lightstrip:`, e)
|
|
785
|
+
return { success: false, error: e }
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
off: async () => {
|
|
789
|
+
log.info(`[${deviceId}] Lightstrip OFF command received`)
|
|
790
|
+
if (!client) {
|
|
791
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
792
|
+
return { success: false }
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
const result = await client.setDeviceState(deviceId, {
|
|
796
|
+
command: 'turnOff',
|
|
797
|
+
parameter: 'default',
|
|
798
|
+
commandType: 'command',
|
|
799
|
+
})
|
|
800
|
+
log.info(`[${deviceId}] Lightstrip turned off successfully`)
|
|
801
|
+
return { success: true, result }
|
|
802
|
+
} catch (e) {
|
|
803
|
+
log.error(`[${deviceId}] Failed to turn off lightstrip:`, e)
|
|
804
|
+
return { success: false, error: e }
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
levelControl: {
|
|
809
|
+
moveToLevel: async (request: any) => {
|
|
810
|
+
const level = request?.level || 0
|
|
811
|
+
// Convert from 0-254 to 0-100
|
|
812
|
+
const brightness = Math.round((level / 254) * 100)
|
|
813
|
+
log.info(`[${deviceId}] Lightstrip brightness change requested: ${brightness}%`)
|
|
814
|
+
if (!client) {
|
|
815
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
816
|
+
return { success: false }
|
|
817
|
+
}
|
|
818
|
+
try {
|
|
819
|
+
const param = Math.max(0, Math.min(100, brightness))
|
|
820
|
+
const result = await client.setDeviceState(deviceId, {
|
|
821
|
+
command: 'setBrightness',
|
|
822
|
+
parameter: String(param),
|
|
823
|
+
commandType: 'command',
|
|
824
|
+
})
|
|
825
|
+
log.info(`[${deviceId}] Lightstrip brightness set to ${param}% successfully`)
|
|
826
|
+
return { success: true, result }
|
|
827
|
+
} catch (e) {
|
|
828
|
+
log.error(`[${deviceId}] Failed to set lightstrip brightness:`, e)
|
|
829
|
+
return { success: false, error: e }
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
colorControl: {
|
|
834
|
+
moveToHueAndSaturation: async (request: any) => {
|
|
835
|
+
const hue = request?.hue || 0
|
|
836
|
+
const saturation = request?.saturation || 0
|
|
837
|
+
log.info(`[${deviceId}] Lightstrip color change requested: hue=${hue}, sat=${saturation}`)
|
|
838
|
+
if (!client) {
|
|
839
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
840
|
+
return { success: false }
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
// Convert hue (0-254) and saturation (0-254) to combined color parameter
|
|
844
|
+
// SwitchBot typically expects RGB or HSV format as parameter
|
|
845
|
+
const colorParam = `${Math.round(hue)},${Math.round(saturation)}`
|
|
846
|
+
const result = await client.setDeviceState(deviceId, {
|
|
847
|
+
command: 'setColor',
|
|
848
|
+
parameter: colorParam,
|
|
849
|
+
commandType: 'command',
|
|
850
|
+
})
|
|
851
|
+
log.info(`[${deviceId}] Lightstrip color set successfully`)
|
|
852
|
+
return { success: true, result }
|
|
853
|
+
} catch (e) {
|
|
854
|
+
log.error(`[${deviceId}] Failed to set lightstrip color:`, e)
|
|
855
|
+
return { success: false, error: e }
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
moveToColorTemperature: async (request: any) => {
|
|
859
|
+
const mireds = request?.colorTemperatureMireds || 400
|
|
860
|
+
// Convert mireds (158-500 typical range) to Kelvin: K = 1000000 / mireds
|
|
861
|
+
const kelvin = Math.round(1000000 / mireds)
|
|
862
|
+
log.info(`[${deviceId}] Lightstrip color temperature change requested: ${mireds} mireds (${kelvin}K)`)
|
|
863
|
+
if (!client) {
|
|
864
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
865
|
+
return { success: false }
|
|
866
|
+
}
|
|
867
|
+
try {
|
|
868
|
+
// Map Kelvin to SwitchBot color temperature parameter (typically 0-100 or specific values)
|
|
869
|
+
// Normalize to 0-100 scale where 0=warm (2700K) and 100=cool (6500K)
|
|
870
|
+
const colorTempParam = Math.max(0, Math.min(100, Math.round(((kelvin - 2700) / 3800) * 100)))
|
|
871
|
+
const result = await client.setDeviceState(deviceId, {
|
|
872
|
+
command: 'setColorTemperature',
|
|
873
|
+
parameter: String(colorTempParam),
|
|
874
|
+
commandType: 'command',
|
|
875
|
+
})
|
|
876
|
+
log.info(`[${deviceId}] Lightstrip color temperature set to ${kelvin}K successfully`)
|
|
877
|
+
return { success: true, result }
|
|
878
|
+
} catch (e) {
|
|
879
|
+
log.error(`[${deviceId}] Failed to set lightstrip color temperature:`, e)
|
|
880
|
+
return { success: false, error: e }
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
case 'humidifier':
|
|
887
|
+
return {
|
|
888
|
+
onOff: {
|
|
889
|
+
on: async () => {
|
|
890
|
+
log.info(`[${deviceId}] Humidifier ON command received`)
|
|
891
|
+
if (!client) {
|
|
892
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
893
|
+
return { success: false }
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
const result = await client.setDeviceState(deviceId, {
|
|
897
|
+
command: 'turnOn',
|
|
898
|
+
parameter: 'default',
|
|
899
|
+
commandType: 'command',
|
|
900
|
+
})
|
|
901
|
+
log.info(`[${deviceId}] Humidifier turned on successfully`)
|
|
902
|
+
return { success: true, result }
|
|
903
|
+
} catch (e) {
|
|
904
|
+
log.error(`[${deviceId}] Failed to turn on humidifier:`, e)
|
|
905
|
+
return { success: false, error: e }
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
off: async () => {
|
|
909
|
+
log.info(`[${deviceId}] Humidifier OFF command received`)
|
|
910
|
+
if (!client) {
|
|
911
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
912
|
+
return { success: false }
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
const result = await client.setDeviceState(deviceId, {
|
|
916
|
+
command: 'turnOff',
|
|
917
|
+
parameter: 'default',
|
|
918
|
+
commandType: 'command',
|
|
919
|
+
})
|
|
920
|
+
log.info(`[${deviceId}] Humidifier turned off successfully`)
|
|
921
|
+
return { success: true, result }
|
|
922
|
+
} catch (e) {
|
|
923
|
+
log.error(`[${deviceId}] Failed to turn off humidifier:`, e)
|
|
924
|
+
return { success: false, error: e }
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
fanControl: {
|
|
929
|
+
setFanSpeed: async (request: any) => {
|
|
930
|
+
const speed = request?.percentSetting || 0
|
|
931
|
+
log.info(`[${deviceId}] Humidifier speed change requested: ${speed}%`)
|
|
932
|
+
if (!client) {
|
|
933
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
934
|
+
return { success: false }
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
// Convert percentage to SwitchBot humidifier speed parameter
|
|
938
|
+
const speedParam = Math.max(1, Math.min(100, speed))
|
|
939
|
+
const result = await client.setDeviceState(deviceId, {
|
|
940
|
+
command: 'setFanSpeed',
|
|
941
|
+
parameter: String(speedParam),
|
|
942
|
+
commandType: 'command',
|
|
943
|
+
})
|
|
944
|
+
log.info(`[${deviceId}] Humidifier speed set to ${speedParam}% successfully`)
|
|
945
|
+
return { success: true, result }
|
|
946
|
+
} catch (e) {
|
|
947
|
+
log.error(`[${deviceId}] Failed to set humidifier speed:`, e)
|
|
948
|
+
return { success: false, error: e }
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
case 'relay':
|
|
955
|
+
return {
|
|
956
|
+
onOff: {
|
|
957
|
+
on: async () => {
|
|
958
|
+
log.info(`[${deviceId}] Relay ON command received`)
|
|
959
|
+
if (!client) {
|
|
960
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
961
|
+
return { success: false }
|
|
962
|
+
}
|
|
963
|
+
try {
|
|
964
|
+
const result = await client.setDeviceState(deviceId, {
|
|
965
|
+
command: 'turnOn',
|
|
966
|
+
parameter: 'default',
|
|
967
|
+
commandType: 'command',
|
|
968
|
+
})
|
|
969
|
+
log.info(`[${deviceId}] Relay turned on successfully`)
|
|
970
|
+
return { success: true, result }
|
|
971
|
+
} catch (e) {
|
|
972
|
+
log.error(`[${deviceId}] Failed to turn on relay:`, e)
|
|
973
|
+
return { success: false, error: e }
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
off: async () => {
|
|
977
|
+
log.info(`[${deviceId}] Relay OFF command received`)
|
|
978
|
+
if (!client) {
|
|
979
|
+
log.warn(`[${deviceId}] No SwitchBot client available`)
|
|
980
|
+
return { success: false }
|
|
981
|
+
}
|
|
982
|
+
try {
|
|
983
|
+
const result = await client.setDeviceState(deviceId, {
|
|
984
|
+
command: 'turnOff',
|
|
985
|
+
parameter: 'default',
|
|
986
|
+
commandType: 'command',
|
|
987
|
+
})
|
|
988
|
+
log.info(`[${deviceId}] Relay turned off successfully`)
|
|
989
|
+
return { success: true, result }
|
|
990
|
+
} catch (e) {
|
|
991
|
+
log.error(`[${deviceId}] Failed to turn off relay:`, e)
|
|
992
|
+
return { success: false, error: e }
|
|
993
|
+
}
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
default:
|
|
999
|
+
return undefined
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function resolveMatterDeviceType(matterApi: any, type: string, createdDeviceType?: any, clusters?: any): any {
|
|
1004
|
+
if (createdDeviceType && typeof createdDeviceType === 'object' && typeof createdDeviceType.with === 'function') {
|
|
1005
|
+
return createdDeviceType
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const lowerType = (typeof createdDeviceType === 'string' && createdDeviceType) ? createdDeviceType.toLowerCase() : (type || '').toLowerCase()
|
|
1009
|
+
|
|
1010
|
+
// Cluster-based upgrade for color lights if descriptor omitted device type.
|
|
1011
|
+
const hasColorControl = !!clusters?.colorControl
|
|
1012
|
+
const inferredType = hasColorControl && lowerType === 'light'
|
|
1013
|
+
? 'lightstrip'
|
|
1014
|
+
: lowerType
|
|
1015
|
+
|
|
1016
|
+
const mappedKey = DEVICE_MATTER_DEVICE_TYPE_KEYS[inferredType] || 'OnOffSwitch'
|
|
1017
|
+
return matterApi?.deviceTypes?.[mappedKey] || matterApi?.deviceTypes?.OnOffSwitch
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
export class SwitchBotHAPPlatform {
|
|
1021
|
+
api: API | undefined
|
|
1022
|
+
log: Logger
|
|
1023
|
+
config: SwitchBotPluginConfig
|
|
1024
|
+
devices: any[] = []
|
|
1025
|
+
// cached accessories restored by Homebridge
|
|
1026
|
+
accessories: Map<string, any>
|
|
1027
|
+
// Track last loaded config to detect changes
|
|
1028
|
+
private lastConfigHash: string = ''
|
|
1029
|
+
private configReloadInterval: NodeJS.Timeout | null = null
|
|
1030
|
+
|
|
1031
|
+
constructor(log: Logger, config: PlatformConfig, api?: API) {
|
|
1032
|
+
this.log = log
|
|
1033
|
+
this.config = (config as any) ?? {}
|
|
1034
|
+
this.api = api
|
|
1035
|
+
this.accessories = new Map()
|
|
1036
|
+
this.log.info('SwitchBot HAP platform initialized')
|
|
1037
|
+
|
|
1038
|
+
// Create/shared SwitchBot client and attach to config so child devices reuse it.
|
|
1039
|
+
try {
|
|
1040
|
+
const client = new SwitchBotClient(this.config)
|
|
1041
|
+
void client.init()
|
|
1042
|
+
;(this.config as any)._client = client
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
this.log.debug('Failed to create shared SwitchBot client', e)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Create/shared SwitchBot client and attach to config so child devices reuse it.
|
|
1048
|
+
try {
|
|
1049
|
+
const client = new SwitchBotClient(this.config)
|
|
1050
|
+
void client.init()
|
|
1051
|
+
;(this.config as any)._client = client
|
|
1052
|
+
} catch (e) {
|
|
1053
|
+
this.log.debug('Failed to create shared SwitchBot client', e)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Wait for Homebridge to finish launching to create/register accessories
|
|
1057
|
+
if (this.api && typeof (this.api as any).on === 'function') {
|
|
1058
|
+
(this.api as any).on('didFinishLaunching', async () => {
|
|
1059
|
+
await this.loadDevicesWithMatterInit()
|
|
1060
|
+
// Start periodic config reload to pick up UI changes
|
|
1061
|
+
this.configReloadInterval = setInterval(() => {
|
|
1062
|
+
void this.loadDevicesWithMatterInit()
|
|
1063
|
+
}, 10000) // Check every 10 seconds
|
|
1064
|
+
})
|
|
1065
|
+
} else {
|
|
1066
|
+
void this.loadDevicesWithMatterInit()
|
|
1067
|
+
// Start periodic config reload to pick up UI changes
|
|
1068
|
+
this.configReloadInterval = setInterval(() => {
|
|
1069
|
+
void this.loadDevicesWithMatterInit()
|
|
1070
|
+
}, 10000) // Check every 10 seconds
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Ensures Matter API is loaded before loading devices.
|
|
1076
|
+
*/
|
|
1077
|
+
private async loadDevicesWithMatterInit() {
|
|
1078
|
+
// Wait for Matter API to be loaded and available before loading devices
|
|
1079
|
+
const maxAttempts = 20 // Wait up to 10 seconds (20 x 500ms)
|
|
1080
|
+
let attempt = 0
|
|
1081
|
+
let matterLoaded = false
|
|
1082
|
+
if (
|
|
1083
|
+
this.api
|
|
1084
|
+
&& typeof (this.api as any).isMatterAvailable === 'function'
|
|
1085
|
+
&& typeof (this.api as any).isMatterEnabled === 'function'
|
|
1086
|
+
&& typeof (this.api as any).loadMatterAPI === 'function'
|
|
1087
|
+
) {
|
|
1088
|
+
if ((this.api as any).isMatterAvailable() && (this.api as any).isMatterEnabled()) {
|
|
1089
|
+
try {
|
|
1090
|
+
await (this.api as any).loadMatterAPI()
|
|
1091
|
+
this.log.info('Homebridge Matter API loaded successfully')
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
this.log.warn('Failed to load Homebridge Matter API', e)
|
|
1094
|
+
}
|
|
1095
|
+
// Wait for api.matter to be available
|
|
1096
|
+
while (attempt < maxAttempts) {
|
|
1097
|
+
if ((this.api as any).matter) {
|
|
1098
|
+
matterLoaded = true
|
|
1099
|
+
break
|
|
1100
|
+
}
|
|
1101
|
+
await new Promise(res => setTimeout(res, 500))
|
|
1102
|
+
attempt++
|
|
1103
|
+
}
|
|
1104
|
+
if (!matterLoaded) {
|
|
1105
|
+
this.log.warn('Matter API did not become available after loadMatterAPI()')
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
await this.loadDevices()
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
private getConfigHash(): string {
|
|
1113
|
+
// Create a simple hash of current device config to detect changes
|
|
1114
|
+
const devices = (this.config as any)?.devices ?? []
|
|
1115
|
+
return JSON.stringify(devices.map((d: any) => ({
|
|
1116
|
+
id: d.deviceId ?? d.id,
|
|
1117
|
+
type: d.configDeviceType ?? d.type,
|
|
1118
|
+
name: d.configDeviceName ?? d.name,
|
|
1119
|
+
})))
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async loadDevices() {
|
|
1123
|
+
const devices = (this.config as any)?.devices ?? []
|
|
1124
|
+
for (const raw of devices) {
|
|
1125
|
+
// Normalize config keys from UI schema to internal shape
|
|
1126
|
+
const d: any = {
|
|
1127
|
+
id: raw.deviceId ?? raw.id,
|
|
1128
|
+
name: raw.configDeviceName ?? raw.name,
|
|
1129
|
+
type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
|
|
1130
|
+
encryptionKey: raw.encryptionKey,
|
|
1131
|
+
keyId: raw.keyId,
|
|
1132
|
+
_raw: raw,
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const type: string = normalizeTypeForMatter(d.type)
|
|
1136
|
+
const windowCoveringTypes = ['curtain', 'curtain3', 'rollershade', 'roller shade', 'blindtilt', 'blind tilt']
|
|
1137
|
+
// Patch: If a config override for matterType is present, update DEVICE_MATTER_CLUSTERS for this device
|
|
1138
|
+
if (windowCoveringTypes.includes(type.toLowerCase()) && typeof raw.matterType === 'number') {
|
|
1139
|
+
if (DEVICE_MATTER_CLUSTERS[type] && DEVICE_MATTER_CLUSTERS[type].windowCovering) {
|
|
1140
|
+
DEVICE_MATTER_CLUSTERS[type].windowCovering.type = raw.matterType
|
|
1141
|
+
this.log.info(`[Matter/Debug] Overriding DEVICE_MATTER_CLUSTERS.${type}.windowCovering.type to`, raw.matterType)
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const deviceOpts: any = { id: d.id, type, name: d.name, encryptionKey: d.encryptionKey, keyId: d.keyId }
|
|
1145
|
+
if (windowCoveringTypes.includes(type.toLowerCase())) {
|
|
1146
|
+
if (typeof raw.matterType === 'number') {
|
|
1147
|
+
deviceOpts.matterType = raw.matterType
|
|
1148
|
+
this.log.info(`[Matter/Debug] Passing matterType override for device ${d.name ?? d.id}: matterType=${raw.matterType}`)
|
|
1149
|
+
} else {
|
|
1150
|
+
this.log.info(`[Matter/Debug] No matterType override for device ${d.name ?? d.id}`)
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
this.log.debug(`[Matter/Debug] Device options for ${d.name ?? d.id}:`, JSON.stringify(deviceOpts, null, 2))
|
|
1154
|
+
|
|
1155
|
+
const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
|
|
1156
|
+
// Auto-detect Matter from Homebridge API, allow manual override via config
|
|
1157
|
+
const matterAvailable = !!(this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.())
|
|
1158
|
+
const matterEnabled = matterAvailable || !!this.config.enableMatter
|
|
1159
|
+
const useMatter = !!(matterEnabled && matterSupported && (!!this.config.preferMatter || matterAvailable))
|
|
1160
|
+
|
|
1161
|
+
try {
|
|
1162
|
+
const created = await createDevice(deviceOpts, this.config, useMatter)
|
|
1163
|
+
this.devices.push(created)
|
|
1164
|
+
// Prefer Matter: try registering to the Matter child bridge first.
|
|
1165
|
+
let matterRegistered = false
|
|
1166
|
+
if (useMatter) {
|
|
1167
|
+
const matterApi = (this.api as any)?.matter
|
|
1168
|
+
if (this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.() && matterApi && typeof matterApi.registerPlatformAccessories === 'function') {
|
|
1169
|
+
try {
|
|
1170
|
+
const createdDesc = await created.createAccessory(this.api)
|
|
1171
|
+
const uuid = matterApi.uuid.generate(`${d.id}`)
|
|
1172
|
+
const defaultClusters = DEVICE_MATTER_CLUSTERS[type.toLowerCase()] || { onOff: { onOff: false } }
|
|
1173
|
+
const clusters = createdDesc.clusters || defaultClusters
|
|
1174
|
+
const deviceType = resolveMatterDeviceType(matterApi, type, createdDesc.deviceType, clusters)
|
|
1175
|
+
const accessory: any = {
|
|
1176
|
+
UUID: uuid,
|
|
1177
|
+
displayName: createdDesc.name || d.name || type,
|
|
1178
|
+
deviceType,
|
|
1179
|
+
manufacturer: createdDesc.manufacturer || 'SwitchBot',
|
|
1180
|
+
model: createdDesc.model || type,
|
|
1181
|
+
serialNumber: createdDesc.serialNumber || d.id,
|
|
1182
|
+
reachable: createdDesc.reachable !== false,
|
|
1183
|
+
firmwareRevision: createdDesc.firmwareRevision || '1.0.0',
|
|
1184
|
+
hardwareRevision: createdDesc.hardwareRevision || '',
|
|
1185
|
+
clusters,
|
|
1186
|
+
handlers: createdDesc.handlers || createMatterHandlers(this.log, d.id, type, (this.config as any)?._client) || undefined,
|
|
1187
|
+
context: { deviceId: d.id, type, _created: true },
|
|
1188
|
+
}
|
|
1189
|
+
this.log.info(`[MatterDebug] Accessory descriptor for ${d.id}:`, JSON.stringify({
|
|
1190
|
+
UUID: accessory.UUID,
|
|
1191
|
+
displayName: accessory.displayName,
|
|
1192
|
+
deviceType: typeof accessory.deviceType === 'object' && accessory.deviceType?.name ? accessory.deviceType.name : accessory.deviceType,
|
|
1193
|
+
clusters: accessory.clusters,
|
|
1194
|
+
context: accessory.context,
|
|
1195
|
+
}, null, 2))
|
|
1196
|
+
await matterApi.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
|
|
1197
|
+
this.accessories.set(uuid, accessory)
|
|
1198
|
+
matterRegistered = true
|
|
1199
|
+
this.log.info(`[MatterDebug] Registered Matter accessory ${d.id} (${type}) with uuid=${uuid}`)
|
|
1200
|
+
} catch (e: any) {
|
|
1201
|
+
this.log.error(`[MatterDebug] Failed to register Matter accessory for ${d.id} (${type}):`, e && (e.stack || e.message || e))
|
|
1202
|
+
if (e && (e.message?.includes('Conformance') || e.message?.includes('enum value Rollershade') || e.message?.includes('Behaviors have errors'))) {
|
|
1203
|
+
try {
|
|
1204
|
+
const uuid = matterApi.uuid.generate(`${d.id}`)
|
|
1205
|
+
const cached = this.accessories.get(uuid)
|
|
1206
|
+
if (cached && typeof this.api?.unregisterPlatformAccessories === 'function') {
|
|
1207
|
+
this.log.warn(`[MatterDebug] Removing cached accessory for ${d.id} due to registration error`)
|
|
1208
|
+
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [cached])
|
|
1209
|
+
this.accessories.delete(uuid)
|
|
1210
|
+
}
|
|
1211
|
+
} catch (cleanupErr) {
|
|
1212
|
+
this.log.warn(`[MatterDebug] Failed to cleanup cached accessory for ${d.id}:`, cleanupErr)
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
} else {
|
|
1217
|
+
this.log.info(`Matter API not available for ${d.id} (${type}); skipping Matter registration and falling back to HAP`)
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// If Matter wasn't registered (either not supported, API missing, or registration failed), fall back to HAP registration.
|
|
1222
|
+
if (!matterRegistered && this.api && (this.api as any).hap) {
|
|
1223
|
+
// Basic HAP accessory creation using homebridge API when available
|
|
1224
|
+
try {
|
|
1225
|
+
const hap = (this.api as any).hap
|
|
1226
|
+
const uuid = hap.uuid.generate(`${d.id}`)
|
|
1227
|
+
// Reuse cached accessory if available by uuid
|
|
1228
|
+
let accessory: any = this.accessories.get(uuid)
|
|
1229
|
+
// If not found by uuid, attempt to find by stored deviceId in accessory.context
|
|
1230
|
+
if (!accessory) {
|
|
1231
|
+
for (const [, a] of this.accessories.entries()) {
|
|
1232
|
+
try {
|
|
1233
|
+
if (a && a.context && a.context.deviceId === d.id) {
|
|
1234
|
+
accessory = a
|
|
1235
|
+
break
|
|
1236
|
+
}
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
// ignore
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (!accessory) {
|
|
1244
|
+
accessory = new (this.api as any).platformAccessory(d.name || type, uuid)
|
|
1245
|
+
// Store device metadata on accessory.context for persistence across restarts
|
|
1246
|
+
try {
|
|
1247
|
+
accessory.context = accessory.context || {}
|
|
1248
|
+
accessory.context.deviceId = d.id
|
|
1249
|
+
accessory.context.type = type
|
|
1250
|
+
} catch (e) {
|
|
1251
|
+
// ignore context failures
|
|
1252
|
+
}
|
|
1253
|
+
// Register new accessory with Homebridge so it's cached
|
|
1254
|
+
try {
|
|
1255
|
+
;(this.api as any).registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
|
|
1256
|
+
} catch (e) {
|
|
1257
|
+
// older API variations may require different registration; ignore if unavailable
|
|
1258
|
+
}
|
|
1259
|
+
this.accessories.set(uuid, accessory)
|
|
1260
|
+
} else {
|
|
1261
|
+
// ensure context includes deviceId (in case restored accessory lacked it)
|
|
1262
|
+
try {
|
|
1263
|
+
accessory.context = accessory.context || {}
|
|
1264
|
+
accessory.context.deviceId = accessory.context.deviceId || d.id
|
|
1265
|
+
accessory.context.type = accessory.context.type || type
|
|
1266
|
+
} catch (e) {
|
|
1267
|
+
// ignore
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
// Add basic service descriptor from device
|
|
1271
|
+
const accDesc = await created.createAccessory?.(this.api)
|
|
1272
|
+
if (accDesc && accDesc.services) {
|
|
1273
|
+
for (const s of accDesc.services) {
|
|
1274
|
+
const Service = hap.Service[s.type] || hap.Service[s.type]
|
|
1275
|
+
if (!Service) {
|
|
1276
|
+
continue
|
|
1277
|
+
}
|
|
1278
|
+
const service = accessory.getService(Service) || accessory.addService(Service)
|
|
1279
|
+
for (const [charName, getterSetterRaw] of Object.entries(s.characteristics || {})) {
|
|
1280
|
+
const getterSetter: any = getterSetterRaw
|
|
1281
|
+
const Characteristic = (hap.Characteristic as any)[charName]
|
|
1282
|
+
if (!Characteristic) {
|
|
1283
|
+
continue
|
|
1284
|
+
}
|
|
1285
|
+
// Apply characteristic props if provided (min/max/step)
|
|
1286
|
+
if (getterSetter && getterSetter.props) {
|
|
1287
|
+
try {
|
|
1288
|
+
service.getCharacteristic(Characteristic).setProps(getterSetter.props)
|
|
1289
|
+
} catch (e) {
|
|
1290
|
+
// ignore setProps failures on older HAP implementations
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Wire simple get/set handlers if provided
|
|
1295
|
+
if (getterSetter && typeof getterSetter.get === 'function') {
|
|
1296
|
+
service.getCharacteristic(Characteristic).onGet(getterSetter.get)
|
|
1297
|
+
}
|
|
1298
|
+
if (getterSetter && typeof getterSetter.set === 'function') {
|
|
1299
|
+
service.getCharacteristic(Characteristic).onSet(getterSetter.set)
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
this.log.info(`Created/updated HAP accessory ${d.id} (${type})`)
|
|
1305
|
+
} catch (e) {
|
|
1306
|
+
this.log.warn('HAP accessory creation failed', e)
|
|
1307
|
+
}
|
|
1308
|
+
} else if (!matterRegistered) {
|
|
1309
|
+
this.log.info(`Created HAP descriptor for ${d.id} (${type}) (API not available to register)`)
|
|
1310
|
+
}
|
|
1311
|
+
} catch (e) {
|
|
1312
|
+
this.log.error(`Failed to create device ${d.id}:`, e as any)
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// Update hash after successfully loading devices
|
|
1316
|
+
this.lastConfigHash = this.getConfigHash()
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Example lifecycle method called by Homebridge
|
|
1320
|
+
async configureAccessory(accessory: any) {
|
|
1321
|
+
// Homebridge calls this for restored cached accessories — keep a reference.
|
|
1322
|
+
try {
|
|
1323
|
+
const uuid = accessory.UUID || accessory.UUID
|
|
1324
|
+
this.accessories.set(uuid, accessory)
|
|
1325
|
+
this.log.info(`Restored cached accessory ${accessory.displayName || uuid}`)
|
|
1326
|
+
} catch (e) {
|
|
1327
|
+
this.log.warn('configureAccessory failed to restore accessory', e)
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Called by Homebridge when a cached Matter accessory is restored
|
|
1332
|
+
configureMatterAccessory?(accessory: any) {
|
|
1333
|
+
try {
|
|
1334
|
+
const uuid = accessory.uuid || accessory.UUID || accessory.uuid
|
|
1335
|
+
this.accessories.set(uuid, accessory)
|
|
1336
|
+
this.log.info(`Restored cached Matter accessory ${accessory.displayName || uuid}`)
|
|
1337
|
+
} catch (e) {
|
|
1338
|
+
this.log.warn('configureMatterAccessory failed to restore accessory', e)
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Matter platform implementation (placeholder)
|
|
1344
|
+
export class SwitchBotMatterPlatform {
|
|
1345
|
+
api: API | undefined
|
|
1346
|
+
log: Logger
|
|
1347
|
+
config: SwitchBotPluginConfig
|
|
1348
|
+
devices: any[] = []
|
|
1349
|
+
accessories: Map<string, any>
|
|
1350
|
+
// Track last loaded config to detect changes
|
|
1351
|
+
private lastConfigHash: string = ''
|
|
1352
|
+
private configReloadInterval: NodeJS.Timeout | null = null
|
|
1353
|
+
|
|
1354
|
+
constructor(log: Logger, config: PlatformConfig, api?: API) {
|
|
1355
|
+
this.log = log
|
|
1356
|
+
this.config = (config as any) ?? {}
|
|
1357
|
+
this.api = api
|
|
1358
|
+
this.accessories = new Map()
|
|
1359
|
+
this.log.info('SwitchBot Matter platform initialized')
|
|
1360
|
+
|
|
1361
|
+
if (this.api && typeof (this.api as any).on === 'function') {
|
|
1362
|
+
;(this.api as any).on('didFinishLaunching', () => {
|
|
1363
|
+
;(async () => {
|
|
1364
|
+
// After launch, perform discovery (if any) and register Matter accessories
|
|
1365
|
+
try {
|
|
1366
|
+
await this.loadDevices()
|
|
1367
|
+
if ((this.api as any).isMatterAvailable?.() && (this.api as any).isMatterEnabled?.() && (this.api as any).matter && typeof (this.api as any).matter.registerPlatformAccessories === 'function') {
|
|
1368
|
+
try {
|
|
1369
|
+
await (this as any).registerMatterAccessories?.()
|
|
1370
|
+
} catch (e) {
|
|
1371
|
+
this.log.warn('registerMatterAccessories failed', e)
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
this.log.warn('Error during Matter platform startup', e)
|
|
1376
|
+
}
|
|
1377
|
+
})()
|
|
1378
|
+
// Start periodic config reload to pick up UI changes
|
|
1379
|
+
this.configReloadInterval = setInterval(() => {
|
|
1380
|
+
void this.checkAndReloadDevices()
|
|
1381
|
+
}, 10000) // Check every 10 seconds
|
|
1382
|
+
})
|
|
1383
|
+
} else {
|
|
1384
|
+
void this.loadDevices()
|
|
1385
|
+
// Start periodic config reload to pick up UI changes
|
|
1386
|
+
this.configReloadInterval = setInterval(() => {
|
|
1387
|
+
void this.checkAndReloadDevices()
|
|
1388
|
+
}, 10000) // Check every 10 seconds
|
|
1389
|
+
}
|
|
1390
|
+
// Create/shared SwitchBot client and attach to config so child devices reuse it.
|
|
1391
|
+
try {
|
|
1392
|
+
const client = new SwitchBotClient(this.config)
|
|
1393
|
+
void client.init()
|
|
1394
|
+
;(this.config as any)._client = client
|
|
1395
|
+
} catch (e) {
|
|
1396
|
+
this.log.debug('Failed to create shared SwitchBot client', e)
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
async loadDevices() {
|
|
1401
|
+
const devices = (this.config as any)?.devices ?? []
|
|
1402
|
+
for (const raw of devices) {
|
|
1403
|
+
// Normalize config keys produced by the UI schema
|
|
1404
|
+
const d: any = {
|
|
1405
|
+
id: raw.deviceId ?? raw.id,
|
|
1406
|
+
name: raw.configDeviceName ?? raw.name,
|
|
1407
|
+
type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
|
|
1408
|
+
_raw: raw,
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
const type: string = normalizeTypeForMatter(d.type)
|
|
1412
|
+
|
|
1413
|
+
const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
|
|
1414
|
+
// Auto-detect Matter from Homebridge API, allow manual override via config
|
|
1415
|
+
const matterAvailable = this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.()
|
|
1416
|
+
const matterEnabled = matterAvailable || !!this.config.enableMatter
|
|
1417
|
+
const useMatter = matterEnabled && matterSupported
|
|
1418
|
+
try {
|
|
1419
|
+
const created = await createDevice({ id: d.id, type, name: d.name }, this.config, useMatter)
|
|
1420
|
+
this.devices.push(created)
|
|
1421
|
+
if (useMatter) {
|
|
1422
|
+
this.log.info(`Prepared Matter accessory for ${d.id} (${type})${matterAvailable ? ' (auto-detected)' : ' (manually enabled)'}`)
|
|
1423
|
+
} else {
|
|
1424
|
+
if (!matterEnabled) {
|
|
1425
|
+
this.log.info(`Skipping Matter for ${d.id} (${type}) - Matter not available on this bridge`)
|
|
1426
|
+
} else if (!matterSupported) {
|
|
1427
|
+
this.log.info(`Skipping Matter for ${d.id} (${type}) - device type not supported`)
|
|
1428
|
+
} else {
|
|
1429
|
+
this.log.info(`Skipping Matter for ${d.id} (${type}) - not supported`)
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
} catch (e) {
|
|
1433
|
+
this.log.error(`Failed to create Matter device ${d.id}:`, e as any)
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
// Update hash after successfully loading devices
|
|
1437
|
+
this.lastConfigHash = this.getConfigHash()
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
private getConfigHash(): string {
|
|
1441
|
+
// Create a simple hash of current device config to detect changes
|
|
1442
|
+
const devices = (this.config as any)?.devices ?? []
|
|
1443
|
+
return JSON.stringify(devices.map((d: any) => ({
|
|
1444
|
+
id: d.deviceId ?? d.id,
|
|
1445
|
+
type: d.configDeviceType ?? d.type,
|
|
1446
|
+
name: d.configDeviceName ?? d.name,
|
|
1447
|
+
})))
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
private async checkAndReloadDevices() {
|
|
1451
|
+
const currentHash = this.getConfigHash()
|
|
1452
|
+
if (currentHash !== this.lastConfigHash) {
|
|
1453
|
+
this.log.info('[SwitchBot] Detected config changes, reloading devices...')
|
|
1454
|
+
// Clear existing devices
|
|
1455
|
+
this.devices = []
|
|
1456
|
+
await this.loadDevices()
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
async configureAccessory(accessory: any) {
|
|
1461
|
+
try {
|
|
1462
|
+
const uuid = accessory.UUID || accessory.UUID
|
|
1463
|
+
this.accessories.set(uuid, accessory)
|
|
1464
|
+
this.log.info(`Restored cached Matter accessory ${accessory.displayName || uuid}`)
|
|
1465
|
+
} catch (e) {
|
|
1466
|
+
this.log.warn('configureAccessory failed to restore Matter accessory', e)
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Homebridge calls this when restoring cached Matter accessories
|
|
1471
|
+
configureMatterAccessory(accessory: any) {
|
|
1472
|
+
try {
|
|
1473
|
+
const uuid = accessory.uuid || accessory.UUID || accessory.uuid
|
|
1474
|
+
this.accessories.set(uuid, accessory)
|
|
1475
|
+
this.log.info(`Restored cached Matter accessory ${accessory.displayName || uuid}`)
|
|
1476
|
+
} catch (e) {
|
|
1477
|
+
this.log.warn('configureMatterAccessory failed to restore Matter accessory', e)
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// Register serialized Matter accessories via Homebridge Matter API
|
|
1482
|
+
async registerMatterAccessories() {
|
|
1483
|
+
if (!this.api) {
|
|
1484
|
+
return
|
|
1485
|
+
}
|
|
1486
|
+
const matterApi = (this.api as any).matter
|
|
1487
|
+
if (!matterApi || typeof matterApi.registerPlatformAccessories !== 'function') {
|
|
1488
|
+
this.log.info('Homebridge Matter API not available; skipping Matter accessory registration')
|
|
1489
|
+
return
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const devices = (this.config as any)?.devices ?? []
|
|
1493
|
+
const accessoriesToRegister: any[] = []
|
|
1494
|
+
|
|
1495
|
+
// Auto-detect Matter from Homebridge API
|
|
1496
|
+
const matterAvailable = this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.()
|
|
1497
|
+
const matterEnabled = matterAvailable || !!this.config.enableMatter
|
|
1498
|
+
|
|
1499
|
+
for (const raw of devices) {
|
|
1500
|
+
const d: any = {
|
|
1501
|
+
id: raw.deviceId ?? raw.id,
|
|
1502
|
+
name: raw.configDeviceName ?? raw.name,
|
|
1503
|
+
type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if (!d.id) {
|
|
1507
|
+
continue
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const type: string = normalizeTypeForMatter(d.type)
|
|
1511
|
+
|
|
1512
|
+
const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
|
|
1513
|
+
const useMatter = matterEnabled && matterSupported
|
|
1514
|
+
if (!useMatter) {
|
|
1515
|
+
continue
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
try {
|
|
1519
|
+
const created = await createDevice({ id: d.id, type, name: d.name }, this.config, true)
|
|
1520
|
+
const createdDesc = await created.createAccessory(this.api)
|
|
1521
|
+
const uuid = matterApi.uuid.generate(`${d.id}`)
|
|
1522
|
+
// Try to find existing restored accessory by deviceId
|
|
1523
|
+
let existing: any | undefined
|
|
1524
|
+
for (const [, a] of this.accessories.entries()) {
|
|
1525
|
+
try {
|
|
1526
|
+
if (a && a.context && a.context.deviceId === d.id) {
|
|
1527
|
+
existing = a
|
|
1528
|
+
break
|
|
1529
|
+
}
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
// ignore
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
if (existing) {
|
|
1536
|
+
// Ensure context and displayName are up to date
|
|
1537
|
+
// Prioritize device-specific Matter clusters (e.g., RVC for vacuum) over generic HAP-derived clusters
|
|
1538
|
+
let clusters = DEVICE_MATTER_CLUSTERS[type.toLowerCase()]
|
|
1539
|
+
if (!clusters) {
|
|
1540
|
+
clusters = existing.clusters || createdDesc.clusters || { onOff: { onOff: false } }
|
|
1541
|
+
}
|
|
1542
|
+
const deviceType = resolveMatterDeviceType(matterApi, type, existing.deviceType || createdDesc.deviceType, clusters)
|
|
1543
|
+
existing.context = existing.context || {}
|
|
1544
|
+
existing.context.deviceId = existing.context.deviceId || d.id
|
|
1545
|
+
existing.context.type = existing.context.type || type
|
|
1546
|
+
existing.deviceType = deviceType
|
|
1547
|
+
existing.manufacturer = existing.manufacturer || createdDesc.manufacturer || 'SwitchBot'
|
|
1548
|
+
existing.model = existing.model || createdDesc.model || type
|
|
1549
|
+
existing.serialNumber = existing.serialNumber || createdDesc.serialNumber || d.id
|
|
1550
|
+
existing.reachable = existing.reachable !== false
|
|
1551
|
+
existing.firmwareRevision = existing.firmwareRevision || createdDesc.firmwareRevision || '1.0.0'
|
|
1552
|
+
existing.hardwareRevision = existing.hardwareRevision || createdDesc.hardwareRevision || ''
|
|
1553
|
+
existing.clusters = clusters
|
|
1554
|
+
existing.handlers = createdDesc.handlers || createMatterHandlers(this.log, d.id, type, (this.config as any)?._client) || undefined
|
|
1555
|
+
existing.displayName = createdDesc.name || d.name || type
|
|
1556
|
+
existing.UUID = existing.UUID || existing.uuid || uuid
|
|
1557
|
+
accessoriesToRegister.push(existing)
|
|
1558
|
+
this.accessories.set(existing.UUID || uuid, existing)
|
|
1559
|
+
} else {
|
|
1560
|
+
// Prioritize device-specific Matter clusters (e.g., RVC for vacuum) over generic HAP-derived clusters
|
|
1561
|
+
let clusters = DEVICE_MATTER_CLUSTERS[type.toLowerCase()]
|
|
1562
|
+
if (!clusters) {
|
|
1563
|
+
clusters = createdDesc.clusters || { onOff: { onOff: false } }
|
|
1564
|
+
}
|
|
1565
|
+
const deviceType = resolveMatterDeviceType(matterApi, type, createdDesc.deviceType, clusters)
|
|
1566
|
+
const serialized: any = {
|
|
1567
|
+
UUID: uuid,
|
|
1568
|
+
displayName: createdDesc.name || d.name || type,
|
|
1569
|
+
deviceType,
|
|
1570
|
+
manufacturer: createdDesc.manufacturer || 'SwitchBot',
|
|
1571
|
+
model: createdDesc.model || type,
|
|
1572
|
+
serialNumber: createdDesc.serialNumber || d.id,
|
|
1573
|
+
reachable: createdDesc.reachable !== false,
|
|
1574
|
+
firmwareRevision: createdDesc.firmwareRevision || '1.0.0',
|
|
1575
|
+
hardwareRevision: createdDesc.hardwareRevision || '',
|
|
1576
|
+
clusters,
|
|
1577
|
+
handlers: createdDesc.handlers || createMatterHandlers(this.log, d.id, type, (this.config as any)?._client) || undefined,
|
|
1578
|
+
context: { deviceId: d.id, type, created: true },
|
|
1579
|
+
}
|
|
1580
|
+
accessoriesToRegister.push(serialized)
|
|
1581
|
+
this.accessories.set(uuid, serialized)
|
|
1582
|
+
}
|
|
1583
|
+
} catch (e) {
|
|
1584
|
+
this.log.warn(`Failed to prepare Matter accessory for ${d.id} (${type})`, e)
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
if (accessoriesToRegister.length > 0) {
|
|
1589
|
+
try {
|
|
1590
|
+
await matterApi.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessoriesToRegister)
|
|
1591
|
+
this.log.info(`Registered ${accessoriesToRegister.length} Matter accessory(ies) with Homebridge`)
|
|
1592
|
+
} catch (e) {
|
|
1593
|
+
this.log.warn('Failed to register Matter accessories', e)
|
|
1594
|
+
}
|
|
1595
|
+
} else {
|
|
1596
|
+
this.log.info('No Matter accessories to register')
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
export default SwitchBotHAPPlatform
|