diff --git a/.vscode/settings.json b/.vscode/settings.json index 57325d61..848d5c50 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -30,5 +30,9 @@ "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features" }, - "codeQL.githubDatabase.update": "never" + "codeQL.githubDatabase.update": "never", + "codeQL.githubDatabase.download": "never", + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + } } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8872aca2..3fde7aa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. This project uses [Semantic Versioning](https://semver.org/) +## [3.5.0](https://github.com/OpenWonderLabs/homebridge-switchbot/releases/tag/v3.5.0) (2024-05-26) + +### What's Changed +- Add Support for `Water Detector` +- Add Support for `Battery Circulator Fan` +- Add BLE support for `Smart Lock` +- Add `K10+` deviceType Support +- Add Support for `maxRetries` and `delayBetweenRetries` on OpenAPI status refreshes based on [#959](https://github.com/OpenWonderLabs/homebridge-switchbot/issues/959#issuecomment-2094879876), Thanks [@sametguzeldev](https://github.com/sametguzeldev) +- Major Refactoring of `device` and `irdevice` files. +- Housekeeping and updated dependencies. + +**Full Changelog**: https://github.com/OpenWonderLabs/homebridge-switchbot/compare/v3.4.0...v3.5.0 + ## [3.4.0](https://github.com/OpenWonderLabs/homebridge-switchbot/releases/tag/v3.4.0) (2024-02-11) ### What's Changed diff --git a/config.schema.json b/config.schema.json index e3643ad8..08acf3b4 100644 --- a/config.schema.json +++ b/config.schema.json @@ -149,6 +149,12 @@ "WoIOSensor" ] }, + { + "title": "Water Detector", + "enum": [ + "Water Detector" + ] + }, { "title": "Meter", "enum": [ @@ -302,9 +308,30 @@ "title": "Hide Hub 2's Temperature Sensor", "type": "boolean", "condition": { - "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && (model.options.devices[arrayIndices].configDeviceType === 'Hub 2') && model.options.devices[arrayIndices].deviceId);" + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType === 'Hub 2' && model.options.devices[arrayIndices].deviceId);" } }, + "convertUnitTo": { + "title": "Convert Hub's Temperature Unit To", + "type": "string", + "condition": { + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType === 'Hub 2' && model.options.devices[arrayIndices].deviceId && !model.options.devices[arrayIndices].hub.hide_temperature);" + }, + "oneOf": [ + { + "title": "Celsius", + "enum": [ + "CELSIUS" + ] + }, + { + "title": "Fahrenheit", + "enum": [ + "FAHRENHEIT" + ] + } + ] + }, "hide_humidity": { "title": "Hide Hub 2's Humidity Sensor", "type": "boolean", @@ -472,14 +499,87 @@ "title": "Hide Meter's Temperature Sensor", "type": "boolean", "condition": { - "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && (model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)' || model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor') && model.options.devices[arrayIndices].deviceId);" + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && (model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)') && model.options.devices[arrayIndices].deviceId);" } }, + "convertUnitTo": { + "title": "Convert Meter's Temperature Unit To", + "type": "string", + "condition": { + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && (model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)') && model.options.devices[arrayIndices].deviceId && !model.options.devices[arrayIndices].meter.hide_temperature);" + }, + "oneOf": [ + { + "title": "Celsius", + "enum": [ + "CELSIUS" + ] + }, + { + "title": "Fahrenheit", + "enum": [ + "FAHRENHEIT" + ] + } + ] + }, "hide_humidity": { "title": "Hide Meter's Humidity Sensor", "type": "boolean", "condition": { - "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && (model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)' || model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor') && model.options.devices[arrayIndices].deviceId);" + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && (model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)') && model.options.devices[arrayIndices].deviceId);" + } + } + } + }, + "iosensor": { + "type": "object", + "properties": { + "hide_temperature": { + "title": "Hide Indoor/Outdoor's Temperature Sensor", + "type": "boolean", + "condition": { + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor' && model.options.devices[arrayIndices].deviceId);" + } + }, + "convertUnitTo": { + "title": "Convert Indoor/Outdoor Sensor's Temperature Unit To", + "type": "string", + "condition": { + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor' && model.options.devices[arrayIndices].deviceId && !model.options.devices[arrayIndices].iosensor.hide_temperature);" + }, + "oneOf": [ + { + "title": "Celsius", + "enum": [ + "CELSIUS" + ] + }, + { + "title": "Fahrenheit", + "enum": [ + "FAHRENHEIT" + ] + } + ] + }, + "hide_humidity": { + "title": "Hide Indoor/Outdoor's Humidity Sensor", + "type": "boolean", + "condition": { + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor' && model.options.devices[arrayIndices].deviceId);" + } + } + } + }, + "waterdetector": { + "type": "object", + "properties": { + "hide_leak": { + "title": "Hide Water Detector's Leak Sensor", + "type": "boolean", + "condition": { + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType === 'Water Detector' && model.options.devices[arrayIndices].deviceId);" } } } @@ -821,14 +921,14 @@ "title": "Hide Lock's Contact Sensor", "type": "boolean", "condition": { - "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType === 'Smart Lock' && model.options.devices[arrayIndices].configDeviceType === 'Smart Lock Pro' && model.options.devices[arrayIndices].deviceId);" + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && (model.options.devices[arrayIndices].configDeviceType === 'Smart Lock' || model.options.devices[arrayIndices].configDeviceType === 'Smart Lock Pro') && model.options.devices[arrayIndices].deviceId);" } }, "activate_latchbutton": { "title": "Activate Latch Button", "type": "boolean", "condition": { - "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType === 'Smart Lock' && model.options.devices[arrayIndices].configDeviceType === 'Smart Lock Pro' && model.options.devices[arrayIndices].deviceId);" + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && (model.options.devices[arrayIndices].configDeviceType === 'Smart Lock' || model.options.devices[arrayIndices].configDeviceType === 'Smart Lock Pro') && model.options.devices[arrayIndices].deviceId);" } } } @@ -840,6 +940,22 @@ "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && (model.options.devices[arrayIndices].configDeviceType === 'Curtain' || model.options.devices[arrayIndices].configDeviceType === 'Curtain3'));" } }, + "maxRetries": { + "title": "Device Max Retries for OpenAPI", + "type": "number", + "placeholder": 5, + "condition": { + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && (model.options.devices[arrayIndices].connectionType === 'OpenAPI' || model.options.devices[arrayIndices].connectionType === 'BLE/OpenAPI'));" + } + }, + "delayBetweenRetries": { + "title": "Device Delay Between Retries for OpenAPI (In Seconds)", + "type": "number", + "placeholder": 3, + "condition": { + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && (model.options.devices[arrayIndices].connectionType === 'OpenAPI' || model.options.devices[arrayIndices].connectionType === 'BLE/OpenAPI'));" + } + }, "maxRetry": { "title": "Max Retries for BLE", "type": "number", @@ -853,28 +969,28 @@ "type": "string", "placeholder": "192.168.7.1", "condition": { - "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && (model.options.devices[arrayIndices].configDeviceType === 'Curtain' || model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor' || model.options.devices[arrayIndices].configDeviceType === 'Hub 2' || model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)'));" + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && (model.options.devices[arrayIndices].configDeviceType === 'Curtain' || model.options.devices[arrayIndices].configDeviceType === 'Water Detector' || model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor' || model.options.devices[arrayIndices].configDeviceType === 'Hub 2' || model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)'));" } }, "mqttOptions": { "title": "MQTT Options", "type": "string", "condition": { - "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && model.options.devices[arrayIndices].mqttURL && (model.options.devices[arrayIndices].configDeviceType === 'Curtain' || model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor' || model.options.devices[arrayIndices].configDeviceType === 'Hub 2' || model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)'));" + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && model.options.devices[arrayIndices].mqttURL && (model.options.devices[arrayIndices].configDeviceType === 'Curtain' || model.options.devices[arrayIndices].configDeviceType === 'Water Detector' || model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor' || model.options.devices[arrayIndices].configDeviceType === 'Hub 2' || model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)'));" } }, "mqttPubOptions": { "title": "MQTT Pub Options", "type": "string", "condition": { - "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && model.options.devices[arrayIndices].mqttURL && (model.options.devices[arrayIndices].configDeviceType === 'Curtain' || model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor' || model.options.devices[arrayIndices].configDeviceType === 'Hub 2' || model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)'));" + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && model.options.devices[arrayIndices].mqttURL && (model.options.devices[arrayIndices].configDeviceType === 'Curtain' || model.options.devices[arrayIndices].configDeviceType === 'Water Detector' || model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor' || model.options.devices[arrayIndices].configDeviceType === 'Hub 2' || model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)'));" } }, "history": { "title": "EVE History", "type": "boolean", "condition": { - "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && (model.options.devices[arrayIndices].configDeviceType === 'Curtain' || model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor' || model.options.devices[arrayIndices].configDeviceType === 'Hub 2' || model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)'));" + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && (model.options.devices[arrayIndices].configDeviceType === 'Curtain' || model.options.devices[arrayIndices].configDeviceType === 'Water Detector' || model.options.devices[arrayIndices].configDeviceType === 'WoIOSensor' || model.options.devices[arrayIndices].configDeviceType === 'Hub 2' || model.options.devices[arrayIndices].configDeviceType === 'Meter' || model.options.devices[arrayIndices].configDeviceType === 'MeterPlus' || model.options.devices[arrayIndices].configDeviceType === 'Meter Plus (JP)'));" } }, "firmware": { @@ -882,14 +998,29 @@ "type": "string", "placeholder": "1.2.8", "condition": { - "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId);" + "functionBody": "return (model.options && model.options.devices && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].deviceId && model.options.devices[arrayIndices].configDeviceType);" } }, "refreshRate": { "title": "Device Refresh Rate", "type": "number", "placeholder": 360, - "description": "Indicates the number of seconds between polls of SwitchBot API.", + "condition": { + "functionBody": "return (model.options && model.options.devices && model.options.devices[arrayIndices].deviceId && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType);" + } + }, + "updateRate": { + "title": "Device Update Rate", + "type": "number", + "placeholder": 360, + "condition": { + "functionBody": "return (model.options && model.options.devices && model.options.devices[arrayIndices].deviceId && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType);" + } + }, + "pushRate": { + "title": "Device Push Rate", + "type": "number", + "placeholder": 360, "condition": { "functionBody": "return (model.options && model.options.devices && model.options.devices[arrayIndices].deviceId && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType);" } @@ -898,7 +1029,7 @@ "title": "Offline as Off", "type": "boolean", "condition": { - "functionBody": "return (model.options && model.options.devices && model.options.devices[arrayIndices].deviceId && !model.options.devices[arrayIndices].hide_device);" + "functionBody": "return (model.options && model.options.devices && model.options.devices[arrayIndices].deviceId && !model.options.devices[arrayIndices].hide_device && model.options.devices[arrayIndices].configDeviceType);" } }, "external": { @@ -1521,17 +1652,30 @@ "type": "string", "placeholder": "http://${FQDN}:${PORT}/${PATH}" }, + "maxRetries": { + "title": "Max Retries for OpenAPI", + "type": "number", + "placeholder": 5 + }, + "delayBetweenRetries": { + "title": "Delay Between Retries for OpenAPI (In Seconds)", + "type": "number", + "placeholder": 3 + }, "refreshRate": { - "title": "Refresh Rate", + "title": "Device Refresh Rate", "type": "number", - "placeholder": 360, - "description": "Indicates the number of seconds between polls of SwitchBot API." + "placeholder": 360 + }, + "updateRate": { + "title": "Device Update Rate", + "type": "number", + "placeholder": 5 }, "pushRate": { - "title": "Push Rate", + "title": "Device Push Rate", "type": "number", - "placeholder": 1, - "description": "Indicates the number of seconds between pushes to SwitchBot API." + "placeholder": 1 }, "logging": { "title": "Logging Setting", @@ -1597,7 +1741,8 @@ "title": "{{ value.configDeviceName || value.deviceId || 'New SwitchBot Device' }}", "expandable": true, "expanded": false, - "orderable": false, + "draggable": true, + "orderable": true, "items": [ "options.devices[].configDeviceName", "options.devices[].deviceId", @@ -1606,10 +1751,13 @@ "options.devices[].connectionType", "options.devices[].webhook", "options.devices[].hub.hide_temperature", + "options.devices[].hub.convertUnitTo", "options.devices[].hub.hide_humidity", "options.devices[].hub.hide_lightsensor", "options.devices[].scanDuration", "options.devices[].disableCaching", + "options.devices[].maxRetries", + "options.devices[].delayBetweenRetries", "options.devices[].maxRetry", "options.devices[].bot.mode", "options.devices[].bot.deviceType", @@ -1617,7 +1765,12 @@ "options.devices[].bot.doublePress", "options.devices[].bot.pushRatePress", "options.devices[].meter.hide_temperature", + "options.devices[].meter.convertUnitTo", "options.devices[].meter.hide_humidity", + "options.devices[].iosensor.hide_temperature", + "options.devices[].iosensor.convertUnitTo", + "options.devices[].iosensor.hide_humidity", + "options.devices[].waterdetector.hide_leak", "options.devices[].humidifier.set_minStep", "options.devices[].humidifier.hide_temperature", "options.devices[].curtain.set_minStep", @@ -1654,7 +1807,18 @@ "options.devices[].mqttPubOptions", "options.devices[].history", "options.devices[].firmware", - "options.devices[].refreshRate", + { + "key": "options.devices[].refreshRate", + "description": "Specifies the interval, in seconds, for retrieving the latest device status from the SwitchBot API. This interval applies only to this specific device." + }, + { + "key": "options.devices[].updateRate", + "description": "Specifies the interval, in seconds, at which this device will request updates from the SwitchBot API while the device is in motion, for Curtain(s) and Blind Tilt(s) only." + }, + { + "key": "options.devices[].pushRate", + "description": "Specifies the interval, in seconds, between pushes to the SwitchBot API for this specific device." + }, "options.devices[].offline", "options.devices[].external", "options.devices[].logging" @@ -1675,7 +1839,8 @@ "title": "{{ value.configDeviceName || value.deviceId || 'New IR Device' }}", "expandable": true, "expanded": false, - "orderable": false, + "draggable": true, + "orderable": true, "items": [ "options.irdevices[].configDeviceName", "options.irdevices[].deviceId", @@ -1717,22 +1882,20 @@ "expanded": false, "items": [ "options.webhookURL", - { - "type": "help", - "helpvalue": "
Refresh Rate
Refresh Rate indicates the number of seconds between polls of SwitchBot API." - }, + "options.maxRetries", + "options.delayBetweenRetries", { "key": "options.refreshRate", - "notitle": true - }, + "description": "Specifies the interval, in seconds, for retrieving the latest device status from the SwitchBot API." + }, { - "type": "help", - "helpvalue": "
Push Rate
Push Rate indicates the number of seconds between pushes to SwitchBot API, Currently only for Curtains." - }, + "key": "options.updateRate", + "description": "Specifies the interval, in seconds, at which devices will request updates from the SwitchBot API while in motion, for Curtain(s) and Blind Tilt(s) only." + }, { "key": "options.pushRate", - "notitle": true - }, + "description": "Specifies the interval, in seconds, between pushes to the SwitchBot API." + }, "options.logging" ] } diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..6890c8c0 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,96 @@ +import pluginJs from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import stylistic from '@stylistic/eslint-plugin' + + +export default tseslint.config({ + plugins: { + '@stylistic': stylistic, + '@typescript-eslint': tseslint.plugin, + }, + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: true, + }, + }, + files: ['**/*.ts'], + ignores: ['.dist/*'], + extends: [ + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + ], + rules: { + '@typescript-eslint/array-type': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + '@stylistic/type-annotation-spacing': 'error', + '@stylistic/quotes': [ + 'warn', + 'single', + ], + '@stylistic/indent': [ + 'warn', + 2, + { + 'SwitchCase': 1, + }, + ], + '@stylistic/linebreak-style': [ + 'warn', + 'unix', + ], + '@stylistic/semi': [ + 'warn', + 'always', + ], + '@stylistic/comma-dangle': [ + 'warn', + 'always-multiline', + ], + '@stylistic/dot-notation': 'off', + 'eqeqeq': 'warn', + 'curly': [ + 'warn', + 'all', + ], + '@stylistic/brace-style': [ + 'warn', + ], + 'prefer-arrow-callback': [ + 'warn', + ], + '@stylistic/max-len': [ + 'warn', + 150, + ], + 'no-console': [ + 'warn', + ], // use the provided Homebridge log method instead + 'no-non-null-assertion': [ + 'off', + ], + '@stylistic/comma-spacing': [ + 'error', + ], + '@stylistic/no-multi-spaces': [ + 'warn', + { + 'ignoreEOLComments': true, + }, + ], + '@stylistic/no-trailing-spaces': [ + 'warn', + ], + '@stylistic/lines-between-class-members': [ + 'warn', + 'always', + { + 'exceptAfterSingleLine': true, + }, + ], + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 376c65dc..272fbd64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@switchbot/homebridge-switchbot", - "version": "3.4.0", + "version": "3.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@switchbot/homebridge-switchbot", - "version": "3.4.0", + "version": "3.5.0", "funding": [ { "type": "Paypal", @@ -19,47 +19,41 @@ ], "license": "ISC", "dependencies": { - "@homebridge/plugin-ui-utils": "^1.0.1", + "@homebridge/plugin-ui-utils": "^1.0.3", "async-mqtt": "^2.6.3", "fakegato-history": "^0.6.4", - "homebridge-lib": "^6.7.3", + "homebridge-lib": "^7.0.1", "rxjs": "^7.8.1", - "undici": "^6.6.2" + "undici": "^6.18.1" }, "devDependencies": { - "@types/node": "^20.11.17", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint": "^8.56.0", - "homebridge": "^1.7.0", - "homebridge-config-ui-x": "4.55.1", - "nodemon": "^3.0.3", - "npm-check-updates": "^16.14.15", - "rimraf": "^5.0.5", + "@eslint/js": "^9.3.0", + "@stylistic/eslint-plugin": "^2.1.0", + "@types/eslint__js": "^8.42.3", + "@types/node": "^20.12.12", + "eslint": "^9.3.0", + "globals": "^15.3.0", + "homebridge": "^1.8.2", + "homebridge-config-ui-x": "4.56.2", + "nodemon": "^3.1.1", + "npm-check-updates": "^16.14.20", + "rimraf": "^5.0.7", "ts-node": "^10.9.2", - "typescript": "^5.3.3" + "typescript": "^5.4.5", + "typescript-eslint": "^8.0.0-alpha.14" }, "engines": { - "homebridge": "^1.7.0", - "node": "^18 || ^20" + "homebridge": "^1.8.2", + "node": "^18 || ^20 || ^22" }, "optionalDependencies": { - "node-switchbot": "2.0.3" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node-switchbot": "2.1.1" } }, "node_modules/@abandonware/bluetooth-hci-socket": { - "version": "0.5.3-11", - "resolved": "https://registry.npmjs.org/@abandonware/bluetooth-hci-socket/-/bluetooth-hci-socket-0.5.3-11.tgz", - "integrity": "sha512-gOFnpLtSzYFSsNgR609TdFUGVvX7dhc8nrarcfmWZNgtlzRS0IJq7/xxpTw/G4SicEkNlT6lDi6eHwurIT+axg==", + "version": "0.5.3-12", + "resolved": "https://registry.npmjs.org/@abandonware/bluetooth-hci-socket/-/bluetooth-hci-socket-0.5.3-12.tgz", + "integrity": "sha512-qo2cBoh94j6RPusaNXSLYI8Bzxuz01Bx3MD80a/QYzhHED/FZ6Y0k2w2kRbfIA2EEhFSCbXrBZDQlpilL4nbxA==", "hasInstallScript": true, "optional": true, "os": [ @@ -82,9 +76,9 @@ } }, "node_modules/@abandonware/noble": { - "version": "1.9.2-24", - "resolved": "https://registry.npmjs.org/@abandonware/noble/-/noble-1.9.2-24.tgz", - "integrity": "sha512-9kP3DWP1IxuG5PYjz4rqGXl3u6JIu654iibR6Teasgk5UsdH9nkTi2teYuUl54yoOz6vjgXNjofwXp3naJvhfQ==", + "version": "1.9.2-25", + "resolved": "https://registry.npmjs.org/@abandonware/noble/-/noble-1.9.2-25.tgz", + "integrity": "sha512-FBgwjWkXF3VLb0V0hdOeeVhuupdfuagkBz9E0Xjfmr8MYuR5JjnXHty0/6Q38d4v//EhX7oZaZXBiZWrhWKKbg==", "hasInstallScript": true, "optional": true, "os": [ @@ -106,19 +100,6 @@ "@abandonware/bluetooth-hci-socket": "^0.5.3-11" } }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.9.tgz", - "integrity": "sha512-oeOFTrYWdWXCvXGB5orvMTJ6gCZ9I6FBjR+M38iKNXCsPxr4xT0RTdg5uz1H7QP8pp74IzPtwritEr+JscqHXQ==", - "dev": true, - "dependencies": { - "core-js-pure": "^3.30.2", - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -156,6 +137,18 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", @@ -166,15 +159,15 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -182,41 +175,31 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, "engines": { - "node": "*" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.3.0.tgz", + "integrity": "sha512-niBqk8iwv96+yuTwjM6bWg8ovzAPF9qkICsGtcoa5/dmqcEMfdwNAX7+/OHcJHc7wj7XqPxH98oAHytFYlw6Sw==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@fastify/accept-negotiator": { @@ -240,15 +223,15 @@ } }, "node_modules/@fastify/ajv-compiler/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", + "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js": "^4.4.1" }, "funding": { "type": "github", @@ -262,25 +245,22 @@ "dev": true }, "node_modules/@fastify/busboy": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", - "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "dev": true, - "dependencies": { - "text-decoding": "^1.0.0" - }, "engines": { "node": ">=14" } }, "node_modules/@fastify/cors": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.4.2.tgz", - "integrity": "sha512-IVynbcPG9eWiJ0P/A1B+KynmiU/yTYbu3ooBUSIeHfca/N1XLb9nIJVCws+YTr2q63MA8Y6QLeXQczEv4npM9g==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-9.0.1.tgz", + "integrity": "sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==", "dev": true, "dependencies": { "fastify-plugin": "^4.0.0", - "mnemonist": "0.39.5" + "mnemonist": "0.39.6" } }, "node_modules/@fastify/deepmerge": { @@ -346,22 +326,20 @@ } }, "node_modules/@fastify/middie/node_modules/path-to-regexp": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", "dev": true }, "node_modules/@fastify/multipart": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-8.0.0.tgz", - "integrity": "sha512-xaH1pGIqYnIJjYs5qG6ryhPSFnWuJIfSXYqEUtzmcyREkMk0SwONd2y+SZ9JXfDmETAC/Ogtc/SRbz+AjZhCkw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-8.2.0.tgz", + "integrity": "sha512-OZ8nsyyoS2TV7Yeu3ZdrdDGsKUTAbfjrKC9jSxGgT2qdgek+BxpWX31ZubTrWMNZyU5xwk4ox6AvTjAbYWjrWg==", "dev": true, "dependencies": { - "@fastify/busboy": "^1.0.0", + "@fastify/busboy": "^2.1.0", "@fastify/deepmerge": "^1.0.0", "@fastify/error": "^3.0.0", - "@fastify/swagger": "^8.3.1", - "@fastify/swagger-ui": "^1.8.0", "fastify-plugin": "^4.0.0", "secure-json-parse": "^2.4.0", "stream-wormhole": "^1.1.0" @@ -381,43 +359,17 @@ } }, "node_modules/@fastify/static": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz", - "integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.2.tgz", + "integrity": "sha512-5opbHpZj29EGVBNgELW6gDkueiFWxjLsLVQQCgKencJctq0aqk3vBlkO97z5It4zaSAb3FXOeAxm7KP2tL/hQA==", "dev": true, "dependencies": { "@fastify/accept-negotiator": "^1.0.0", "@fastify/send": "^2.0.0", "content-disposition": "^0.5.3", "fastify-plugin": "^4.0.0", - "glob": "^8.0.1", - "p-limit": "^3.1.0" - } - }, - "node_modules/@fastify/swagger": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-8.14.0.tgz", - "integrity": "sha512-sGiznEb3rl6pKGGUZ+JmfI7ct5cwbTQGo+IjewaTvtzfrshnryu4dZwEsjw0YHABpBA+kCz3kpRaHB7qpa67jg==", - "dev": true, - "dependencies": { - "fastify-plugin": "^4.0.0", - "json-schema-resolver": "^2.0.0", - "openapi-types": "^12.0.0", - "rfdc": "^1.3.0", - "yaml": "^2.2.2" - } - }, - "node_modules/@fastify/swagger-ui": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-1.10.2.tgz", - "integrity": "sha512-f2mRqtblm6eRAFQ3e8zSngxVNEtiYY7rISKQVjPA++ZsWc5WYlPVTb6Bx0G/zy0BIoucNqDr/Q2Vb/kTYkOq1A==", - "dev": true, - "dependencies": { - "@fastify/static": "^6.0.0", - "fastify-plugin": "^4.0.0", - "openapi-types": "^12.0.2", - "rfdc": "^1.3.0", - "yaml": "^2.2.2" + "fastq": "^1.17.0", + "glob": "^10.3.4" } }, "node_modules/@gar/promisify": { @@ -427,9 +379,9 @@ "dev": true }, "node_modules/@homebridge/ciao": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.1.8.tgz", - "integrity": "sha512-Atn8+vwYtfI/J6nYCOVm4uVBAmiQO4rPi0umVbh766cf/OsVxQ+Qedbo9lxIf15iDsMbBlDV7T1wATdHqI5lXw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.2.0.tgz", + "integrity": "sha512-2Qa8MVC7Q5DKH6iXh6cRvqz9VJYVpVZ+whHKrnr8YdPkXxc67kiQ9IOxMb0ydokDTETBVyXgr1m+HrheBtqDoQ==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -441,27 +393,40 @@ "ciao-bcs": "lib/bonjour-conformance-testing.js" }, "engines": { - "node": ">=14" + "node": "^18 || ^20" } }, "node_modules/@homebridge/dbus-native": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.5.1.tgz", - "integrity": "sha512-7xXz3R1W/kcbfQOGp32y4K7etqtowICR1vpx8j85KwPYXbNQrgiZ3zcwDYgDGBWq3FD9xzsW7h4YWJ4vTR2seQ==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.6.0.tgz", + "integrity": "sha512-xObqQeYHTXmt6wsfj10+krTo4xbzR9BgUfX2aQ+edDC9nc4ojfzLScfXCh3zluAm6UCowKw+AFfXn6WLWUOPkg==", "dev": true, "dependencies": { "@homebridge/long": "^5.2.1", - "@homebridge/put": "~0.0.8", - "event-stream": "^4.0.0", - "hexy": "^0.2.10", + "@homebridge/put": "^0.0.8", + "event-stream": "^4.0.1", + "hexy": "^0.3.5", "minimist": "^1.2.6", - "safe-buffer": "^5.1.1", - "xml2js": "^0.5.0" + "safe-buffer": "^5.1.2", + "xml2js": "^0.6.2" }, "bin": { "dbus2js": "bin/dbus2js.js" } }, + "node_modules/@homebridge/hap-client": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@homebridge/hap-client/-/hap-client-1.10.0.tgz", + "integrity": "sha512-jbCfsrT97EF9IkW6Wj9fq0ALeSa1y+3UtZ2V+QsacHBd4cfrBr8S5UvlgU+zemctb/EllxMA8S92XqWg5yL2UA==", + "dev": true, + "dependencies": { + "axios": "^1.6.8", + "bonjour-service": "^1.2.1", + "decamelize": "^5.0.1", + "inflection": "^3.0.0", + "source-map-support": "^0.5.21" + } + }, "node_modules/@homebridge/long": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/@homebridge/long/-/long-5.2.1.tgz", @@ -480,9 +445,9 @@ } }, "node_modules/@homebridge/plugin-ui-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@homebridge/plugin-ui-utils/-/plugin-ui-utils-1.0.1.tgz", - "integrity": "sha512-Qxpu+HTb5F3tz6iV+gls/snzKPcP/9lOHwoV8IpJlXOeVWj3QMeMuw19dHH8ggLPMm4GaKuObrWjU9yOMENKkw==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@homebridge/plugin-ui-utils/-/plugin-ui-utils-1.0.3.tgz", + "integrity": "sha512-p2S/czGYNRnRtMICxBUk4Uar+KCezfyxjqfStfxKgykD2082SNayVDncYUK1xRai78EGHCbif9eoyrmDweh4tQ==" }, "node_modules/@homebridge/put": { "version": "0.0.8", @@ -494,12 +459,12 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -507,28 +472,6 @@ "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -543,11 +486,24 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -577,29 +533,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "devOptional": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "devOptional": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -616,9 +549,9 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "engines": { "node": ">=6.0.0" @@ -641,9 +574,9 @@ } }, "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" }, "node_modules/@lukeed/csprng": { "version": "1.1.0", @@ -695,20 +628,11 @@ "node": ">= 6.0.0" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@mapbox/node-pre-gyp/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "optional": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -738,22 +662,11 @@ "node": ">= 6" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "optional": true, "dependencies": { "glob": "^7.1.3" @@ -765,22 +678,27 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true + }, "node_modules/@nestjs/axios": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", - "integrity": "sha512-VlOZhAGDmOoFdsmewn8AyClAdGpKXQQaY1+3PGB+g6ceurGIdTxZgRX3VXc1T6Zs60PedWjg3A82TDOB05mrzQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.2.tgz", + "integrity": "sha512-Z6GuOUdNQjP7FX+OuV2Ybyamse+/e0BFdTWBX5JxpBDKA+YkdLynDgG6HTF04zy6e9zPa19UX0WA2VDoehwhXQ==", "dev": true, "peerDependencies": { "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "axios": "^1.3.1", - "reflect-metadata": "^0.1.12", "rxjs": "^6.0.0 || ^7.0.0" } }, "node_modules/@nestjs/common": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.0.tgz", - "integrity": "sha512-DGv34UHsZBxCM3H5QGE2XE/+oLJzz5+714JQjBhjD9VccFlQs3LRxo/epso4l7nJIiNlZkPyIUC8WzfU/5RTsQ==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.7.tgz", + "integrity": "sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw==", "dev": true, "dependencies": { "iterare": "1.2.1", @@ -794,7 +712,7 @@ "peerDependencies": { "class-transformer": "*", "class-validator": "*", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { @@ -807,9 +725,9 @@ } }, "node_modules/@nestjs/core": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.0.tgz", - "integrity": "sha512-N06P5ncknW/Pm8bj964WvLIZn2gNhHliCBoAO1LeBvNImYkecqKcrmLbY49Fa1rmMfEM3MuBHeDys3edeuYAOA==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.7.tgz", + "integrity": "sha512-hsdlnfiQ3kgqHL5k7js3CU0PV7hBJVi+LfFMgCkoagRxNMf67z0GFGeOV2jk5d65ssB19qdYsDa1MGVuEaoUpg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -829,7 +747,7 @@ "@nestjs/microservices": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/websockets": "^10.0.0", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { @@ -858,15 +776,15 @@ } }, "node_modules/@nestjs/mapped-types": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.4.tgz", - "integrity": "sha512-xl+gUSp0B+ln1VSNoUftlglk8dfpUes3DHGxKZ5knuBxS5g2H/8p9/DSBOYWUfO5f4u9s6ffBPZ71WO+tbe5SA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", "dev": true, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "class-transformer": "^0.4.0 || ^0.5.0", "class-validator": "^0.13.0 || ^0.14.0", - "reflect-metadata": "^0.1.12" + "reflect-metadata": "^0.1.12 || ^0.2.0" }, "peerDependenciesMeta": { "class-transformer": { @@ -888,16 +806,16 @@ } }, "node_modules/@nestjs/platform-fastify": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-10.3.0.tgz", - "integrity": "sha512-ka4r/cPWM5y/dXoi9dj6pn1o3WLnfImy2bT3aYVasiDsJff2cd3h/ThugwxjdH0BHUpLSPnawEGzADAcO8Fqug==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-10.3.7.tgz", + "integrity": "sha512-J7ICAOC/zTSfJLTWwvVLK9LON+Dw/NdgPQwPBsi3GNIw3TLLXLrQ4WXnHNER8gnXS3sfKOIngRLusMirLqYjEQ==", "dev": true, "dependencies": { - "@fastify/cors": "8.4.2", + "@fastify/cors": "9.0.1", "@fastify/formbody": "7.4.0", "@fastify/middie": "8.3.0", - "fastify": "4.25.1", - "light-my-request": "5.11.0", + "fastify": "4.26.2", + "light-my-request": "5.12.0", "path-to-regexp": "3.2.0", "tslib": "2.6.2" }, @@ -906,7 +824,7 @@ "url": "https://opencollective.com/nest" }, "peerDependencies": { - "@fastify/static": "^6.0.0", + "@fastify/static": "^6.0.0 || ^7.0.0", "@fastify/view": "^7.0.0 || ^8.0.0", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0" @@ -921,12 +839,12 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.0.tgz", - "integrity": "sha512-Rdpk9OdsvJfsmVRtg4/0+cUdvOgBEb3F4zo2r4SBgxb0eaR3BHbhbXTJH/U7NvREIvvYbtSNoWI+h2taUEkXwg==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.7.tgz", + "integrity": "sha512-T9VbVgEUnbid/RiywN9/8YQ8pAGDP++0nX73l4kIWeDWkz5DEh4aLB7O/JvLA3/xRHdjTZ4RiRZazwqSWi1Sog==", "dev": true, "dependencies": { - "socket.io": "4.7.2", + "socket.io": "4.7.5", "tslib": "2.6.2" }, "funding": { @@ -940,24 +858,25 @@ } }, "node_modules/@nestjs/swagger": { - "version": "7.1.17", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.17.tgz", - "integrity": "sha512-ASCxBrvMEN2o/8vEEmrIPMNzrr/hVi7QIR4y1oNYvoBNXHuwoF1VSI3+4Rq/3xmwVnVveJxHlBIs2u5xY9VgGQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.1.tgz", + "integrity": "sha512-LUC4mr+5oAleEC/a2j8pNRh1S5xhKXJ1Gal5ZdRjt9XebQgbngXCdW7JTA9WOEcwGtFZN9EnKYdquzH971LZfw==", "dev": true, "dependencies": { - "@nestjs/mapped-types": "2.0.4", + "@microsoft/tsdoc": "^0.14.2", + "@nestjs/mapped-types": "2.0.5", "js-yaml": "4.1.0", "lodash": "4.17.21", "path-to-regexp": "3.2.0", - "swagger-ui-dist": "5.10.3" + "swagger-ui-dist": "5.11.2" }, "peerDependencies": { - "@fastify/static": "^6.0.0", + "@fastify/static": "^6.0.0 || ^7.0.0", "@nestjs/common": "^9.0.0 || ^10.0.0", "@nestjs/core": "^9.0.0 || ^10.0.0", "class-transformer": "*", "class-validator": "*", - "reflect-metadata": "^0.1.12" + "reflect-metadata": "^0.1.12 || ^0.2.0" }, "peerDependenciesMeta": { "@fastify/static": { @@ -972,9 +891,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.0.tgz", - "integrity": "sha512-1cqh46s4iHLytExSWcvp/58pMZFAT7pQ59IAYQCSZz5xFq0lEGxd36C982KyROQIHfno8E+FWm71UhgVTwKsyA==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.7.tgz", + "integrity": "sha512-iYdsWiRNPUy0XzPoW44bx2MW1griuraTr5fNhoe2rUSNO0mEW1aeXp4v56KeZDLAss31WbeckC5P3N223Fys5g==", "dev": true, "dependencies": { "iterare": "1.2.1", @@ -985,7 +904,7 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-socket.io": "^10.0.0", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { @@ -1030,34 +949,34 @@ } }, "node_modules/@npmcli/agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.1.tgz", - "integrity": "sha512-H4FrOVtNyWC8MUwL3UfjOsAihHvT1Pe8POj3JvjXhSTJipsZMtgUALCT4mGyYZNxymkUfOw3PUj6dE4QPp6osQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", "optional": true, "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.3" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", "optional": true, "engines": { "node": "14 || >=16.14" } }, "node_modules/@npmcli/fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "devOptional": true, "dependencies": { "semver": "^7.3.5" @@ -1110,16 +1029,16 @@ } }, "node_modules/@npmcli/installed-package-contents": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz", - "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", "dev": true, "dependencies": { "npm-bundled": "^3.0.0", "npm-normalize-package-bin": "^3.0.0" }, "bin": { - "installed-package-contents": "lib/index.js" + "installed-package-contents": "bin/index.js" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -1139,20 +1058,11 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/@npmcli/move-file/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@npmcli/move-file/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -1169,18 +1079,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@npmcli/move-file/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@npmcli/move-file/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -1197,6 +1095,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -1289,6 +1188,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "delegates": "^1.0.0", @@ -1299,13 +1199,12 @@ } }, "node_modules/@npmcli/run-script/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/@npmcli/run-script/node_modules/cacache": { @@ -1337,19 +1236,11 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/@npmcli/run-script/node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@npmcli/run-script/node_modules/cacache/node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -1377,6 +1268,12 @@ "node": ">=10" } }, + "node_modules/@npmcli/run-script/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "node_modules/@npmcli/run-script/node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -1393,6 +1290,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -1412,6 +1310,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -1491,18 +1390,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/@npmcli/run-script/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@npmcli/run-script/node_modules/minipass": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", @@ -1615,6 +1502,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "are-we-there-yet": "^3.0.0", @@ -1630,6 +1518,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -1641,6 +1530,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@npmcli/run-script/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/@npmcli/run-script/node_modules/socks-proxy-agent": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", @@ -1667,6 +1562,20 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/run-script/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@npmcli/run-script/node_modules/unique-filename": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", @@ -1771,33 +1680,10 @@ "@otplib/plugin-thirty-two": "^12.0.1" } }, - "node_modules/@oznu/hap-client": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@oznu/hap-client/-/hap-client-1.9.0.tgz", - "integrity": "sha512-dtx8SNLzT62VVwN2YwBVooLyKBrBphddVvYda8CqY5BHLzjK8SMDOF63t2nvp+MLwzWXKfpmcX1w+qS3V8J9Gw==", - "dev": true, - "dependencies": { - "axios": "^0.27.2", - "bonjour-service": "^1.0.12", - "decamelize": "^3.2.0", - "inflection": "^1.13.2", - "source-map-support": "^0.5.19" - } - }, - "node_modules/@oznu/hap-client/node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "optional": true, "engines": { @@ -1915,28 +1801,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@sigstore/sign/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@sigstore/sign/node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -2072,11 +1936,362 @@ } }, "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true }, + "node_modules/@stylistic/eslint-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.1.0.tgz", + "integrity": "sha512-cBBowKP2u/+uE5CzgH5w8pE9VKqcM7BXdIDPIbGt2rmLJGnA6MJPr9vYGaqgMoJFs7R/FzsMQerMvvEP40g2uw==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "2.1.0", + "@stylistic/eslint-plugin-jsx": "2.1.0", + "@stylistic/eslint-plugin-plus": "2.1.0", + "@stylistic/eslint-plugin-ts": "2.1.0", + "@types/eslint": "^8.56.10" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.1.0.tgz", + "integrity": "sha512-gdXUjGNSsnY6nPyqxu6lmDTtVrwCOjun4x8PUn0x04d5ucLI74N3MT1Q0UhdcOR9No3bo5PGDyBgXK+KmD787A==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.10", + "acorn": "^8.11.3", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-jsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-2.1.0.tgz", + "integrity": "sha512-mMD7S+IndZo2vxmwpHVTCwx2O1VdtE5tmpeNwgaEcXODzWV1WTWpnsc/PECQKIr/mkLPFWiSIqcuYNhQ/3l6AQ==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "^2.1.0", + "@types/eslint": "^8.56.10", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-plus": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-plus/-/eslint-plugin-plus-2.1.0.tgz", + "integrity": "sha512-S5QAlgYXESJaSBFhBSBLZy9o36gXrXQwWSt6QkO+F0SrT9vpV5JF/VKoh+ojO7tHzd8Ckmyouq02TT9Sv2B0zQ==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.10", + "@typescript-eslint/utils": "^7.8.0" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/scope-manager": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz", + "integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz", + "integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz", + "integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz", + "integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/typescript-estree": "7.10.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz", + "integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@stylistic/eslint-plugin-ts": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.1.0.tgz", + "integrity": "sha512-2ioFibufHYBALx2TBrU4KXovCkN8qCqcb9yIHc0fyOfTaO5jw4d56WW7YRcF3Zgde6qFyXwAN6z/+w4pnmos1g==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "2.1.0", + "@types/eslint": "^8.56.10", + "@typescript-eslint/utils": "^7.8.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/scope-manager": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz", + "integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz", + "integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz", + "integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz", + "integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/typescript-estree": "7.10.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz", + "integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -2099,9 +2314,9 @@ } }, "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "dev": true }, "node_modules/@tsconfig/node12": { @@ -2144,6 +2359,30 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", @@ -2159,6 +2398,31 @@ "@types/node": "*" } }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "dev": true, + "dependencies": { + "@types/eslint": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -2181,24 +2445,24 @@ } }, "node_modules/@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, - "node_modules/@types/semver": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", - "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "node_modules/@types/semver-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/semver-utils/-/semver-utils-1.1.3.tgz", + "integrity": "sha512-T+YwkslhsM+CeuhYUxyAjWm7mJ5am/K10UX40RuA6k6Lc7eGtq8iY2xOzy7Vq0GOqhl/xZl5l2FwURZMTPTUww==", "dev": true }, "node_modules/@types/validator": { - "version": "13.11.9", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", - "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==", + "version": "13.11.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz", + "integrity": "sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==", "dev": true }, "node_modules/@types/w3c-web-usb": { @@ -2208,33 +2472,31 @@ "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0-alpha.16.tgz", + "integrity": "sha512-ZDVgR/z28jg3CPzQJqFIOQ/gshqf3NDw7zCu2jTeAYqtyXpCsAkAivvkeuuuXCypRl53cK16qDPlCguUCZW5Ow==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.0.0-alpha.16", + "@typescript-eslint/type-utils": "8.0.0-alpha.16", + "@typescript-eslint/utils": "8.0.0-alpha.16", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.16", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2243,26 +2505,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.0-alpha.16.tgz", + "integrity": "sha512-L8eX2ggDQqb986+P9FZVsl/4M0vPplvgVzPkFFtPtsP2rVRSFpzGidZGzNN73RBq2G5KnWo87sx2mUrJ+99OZQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/scope-manager": "8.0.0-alpha.16", + "@typescript-eslint/types": "8.0.0-alpha.16", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.16", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.16", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2271,16 +2533,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.0-alpha.16.tgz", + "integrity": "sha512-SsN6Kf+sBK62CgDkW4XHZYDqCDwOY2d1Q4aUAOTcohhw06HiXYbY5xQ23GqOV2BL9TaKL+HuyyP+LLZ1aIG8FQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "8.0.0-alpha.16", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.16" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2288,26 +2550,23 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.0-alpha.16.tgz", + "integrity": "sha512-g5GJ0sB6WLu71fkPlMe9JV1o3p6AKAN0vUfg4XGyYPLSElRYdMMy4Nuq1Snq2Gqs1rceomHrogp5v/qH7Iq7ig==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.16", + "@typescript-eslint/utils": "8.0.0-alpha.16", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -2315,12 +2574,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.0-alpha.16.tgz", + "integrity": "sha512-06m3u1WIT49iYLK2GJWdT7Lmx54pX8imcW06AFnmgMXYDQsTZDdNXpHM6vwwL29LAWDv44j8g+eDPjJ4UNNiCA==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2328,22 +2587,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.0-alpha.16.tgz", + "integrity": "sha512-q5FvwPYGHmDF4/J7ssWMBHKDRY/3ar1PNoKTMYh/1foSCJ2e/Hv/GTuc63h03xi12IRyTn8R/M/56vH6qd+rSQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/types": "8.0.0-alpha.16", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.16", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2355,53 +2614,80 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.0-alpha.16.tgz", + "integrity": "sha512-u7mFyhJ4/jX7VaGieK+BC+PynvCH8fdr4Gie4RXO9bclvGAvMTzk62UZ65t90KN25M9/tvodxUoaZS4W4MQSNg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "@typescript-eslint/scope-manager": "8.0.0-alpha.16", + "@typescript-eslint/types": "8.0.0-alpha.16", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.16" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.0-alpha.16.tgz", + "integrity": "sha512-vSmfkS6FVBW1lhuf700XjcbQXtoXg3Aqbi+axsFYPNr/6oEkpLRonbKMxBzj4cGTnL/3sJl+gDVQSS7fVHWz3A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "8.0.0-alpha.16", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, "node_modules/abbrev": { "version": "1.1.1", @@ -2471,9 +2757,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dependencies": { "debug": "^4.3.4" }, @@ -2540,15 +2826,15 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", + "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js": "^4.4.1" }, "funding": { "type": "github", @@ -2570,6 +2856,26 @@ "string-width": "^4.1.0" } }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2583,6 +2889,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2606,22 +2913,29 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "devOptional": true }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", - "dev": true - }, "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", "optional": true, "dependencies": { "delegates": "^1.0.0", @@ -2696,9 +3010,12 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz", - "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2707,24 +3024,22 @@ } }, "node_modules/avvio": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", - "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.2.tgz", + "integrity": "sha512-st8e519GWHa/azv8S87mcJvZs4WsgTBjOw/Ih1CP6u+8SZvcOeAYNG6JbsIrAUUJJ7JfmrnOkR8ipDS+u9SIRQ==", "dev": true, "dependencies": { "@fastify/error": "^3.3.0", - "archy": "^1.0.0", - "debug": "^4.0.0", "fastq": "^1.17.1" } }, "node_modules/axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", "dev": true, "dependencies": { - "follow-redirects": "^1.15.4", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -2799,12 +3114,15 @@ } }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bl": { @@ -2824,141 +3142,76 @@ "dev": true }, "node_modules/bonjour-hap": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.6.4.tgz", - "integrity": "sha512-a76r95/qTAP5hOEZZhRoiosyFSVPPRSVev09Jh8yDf3JDKyrzELLf0vpQCuEXFueb9DcV9UJf2Jv3dktyuPBng==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.7.2.tgz", + "integrity": "sha512-BzOdOSIpXqjE1hejVNhj1T7E5YazPNG7cMOph5jQfzf1nF2yO18FSxuIg2zDMa4tFxhNC5d+U+0hT2bQkC5nTw==", "dependencies": { "array-flatten": "^2.1.2", - "deep-equal": "^2.0.5", - "ip": "^1.1.8", + "deep-equal": "^2.2.3", "multicast-dns": "^7.2.5", "multicast-dns-service-types": "^1.1.0" } }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dev": true, "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "node_modules/boxen": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", - "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", - "dev": true, - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^7.0.1", - "chalk": "^5.2.0", - "cli-boxes": "^3.0.0", - "string-width": "^5.1.2", - "type-fest": "^2.13.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/boxen/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/boxen/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/boxen/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", + "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-align": "^3.0.1", + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0" }, "engines": { - "node": ">=12" + "node": ">=14.16" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/boxen/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "node_modules/boxen/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "engines": { - "node": ">=12.20" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "devOptional": true, + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3021,19 +3274,10 @@ "node": ">=0.2.0" } }, - "node_modules/builtins": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", - "dev": true, - "dependencies": { - "semver": "^7.0.0" - } - }, "node_modules/cacache": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz", - "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==", + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.3.tgz", + "integrity": "sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==", "optional": true, "dependencies": { "@npmcli/fs": "^3.1.0", @@ -3053,32 +3297,10 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/cacache/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "optional": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/cacache/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", "optional": true, "engines": { "node": "14 || >=16.14" @@ -3124,14 +3346,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", - "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "set-function-length": "^1.2.0" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -3177,6 +3400,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3255,14 +3479,14 @@ "dev": true }, "node_modules/class-validator": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", - "integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", "dev": true, "dependencies": { - "@types/validator": "^13.7.10", - "libphonenumber-js": "^1.10.14", - "validator": "^13.7.0" + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" } }, "node_modules/clean-stack": { @@ -3311,9 +3535,9 @@ } }, "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, "dependencies": { "string-width": "^4.2.0" @@ -3325,6 +3549,26 @@ "@colors/colors": "1.5.0" } }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -3338,6 +3582,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3348,7 +3593,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true }, "node_modules/color-support": { "version": "1.1.3", @@ -3372,12 +3618,12 @@ } }, "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", "dev": true, "engines": { - "node": ">= 10" + "node": ">=18" } }, "node_modules/commist": { @@ -3468,25 +3714,14 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, "engines": { "node": ">= 0.6" } }, - "node_modules/core-js-pure": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.35.1.tgz", - "integrity": "sha512-zcIdi/CL3MWbBJYo5YCeVAAx+Sy9yJE9I3/u9LkFABwbeaPhTMRWraM8mYFp9jW5Z50hOy7FVzCc8dCrpZqtIQ==", - "dev": true, - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -3588,15 +3823,15 @@ } }, "node_modules/decamelize": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-3.2.0.tgz", - "integrity": "sha512-4TgkVUsmmu7oCSyGBm5FvfMoACuoh9EOidm7V5/J2X2djAwwt57qb3F2KMP2ITqODTCSwb+YRV+0Zqrv18k/hw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", + "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", "dev": true, - "dependencies": { - "xregexp": "^4.2.4" - }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -3691,17 +3926,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz", - "integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { + "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.2", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -3745,9 +3982,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "devOptional": true, "engines": { "node": ">=8" @@ -3774,12 +4011,6 @@ "node": ">=8" } }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -3791,18 +4022,6 @@ "node": ">=6" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -3870,14 +4089,14 @@ } }, "node_modules/duplexify": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", - "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" + "stream-shift": "^1.0.2" } }, "node_modules/eastasianwidth": { @@ -3895,9 +4114,9 @@ } }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "devOptional": true }, "node_modules/encoding": { @@ -3992,6 +4211,17 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "devOptional": true }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -4050,41 +4280,37 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.3.0.tgz", + "integrity": "sha512-5Iv4CsZW030lpUqHBapdPo3MJetAPtejVW8B84GIcIIv8+ohFaddXsrn1Gn8uD9ijDb+kcYKFUVmC8qG8B2ORQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.3.0", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", + "eslint-scope": "^8.0.1", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -4098,74 +4324,52 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", + "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", "dev": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.11.3", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -4345,14 +4549,14 @@ "dev": true }, "node_modules/fast-json-stringify": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.12.0.tgz", - "integrity": "sha512-7Nnm9UPa7SfHRbHVA1kJQrGXCRzB7LMlAAqHXQFkEQqueJm1V8owm0FsE/2Do55/4CcdhwiLQERaKomOnKQkyA==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.0.tgz", + "integrity": "sha512-A4bg6E15QrkuVO3f0SwIASgzMzR6XC4qTyTqhf3hYXy0iazbAdZKwkE+ox4WgzKyzM6ygvbdq3r134UjOaaAnA==", "dev": true, "dependencies": { "@fastify/merge-json-schemas": "^0.1.0", "ajv": "^8.10.0", - "ajv-formats": "^2.1.1", + "ajv-formats": "^3.0.1", "fast-deep-equal": "^3.1.3", "fast-uri": "^2.1.0", "json-schema-ref-resolver": "^1.0.1", @@ -4360,21 +4564,38 @@ } }, "node_modules/fast-json-stringify/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", + "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js": "^4.4.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -4403,9 +4624,9 @@ } }, "node_modules/fast-redact": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", - "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", "dev": true, "engines": { "node": ">=6" @@ -4433,19 +4654,29 @@ "dev": true }, "node_modules/fastify": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.25.1.tgz", - "integrity": "sha512-D8d0rv61TwqoAS7lom2tvIlgVMlx88lLsiwXyWNjA7CU/LC/mx/Gp2WAlC0S/ABq19U+y/aRvYFG5xLUu2aMrg==", + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.2.tgz", + "integrity": "sha512-90pjTuPGrfVKtdpLeLzND5nyC4woXZN5VadiNQCicj/iJU4viNHKhsAnb7jmv1vu2IzkLXyBiCzdWuzeXgQ5Ug==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "dependencies": { "@fastify/ajv-compiler": "^3.5.0", "@fastify/error": "^3.4.0", "@fastify/fast-json-stringify-compiler": "^4.3.0", "abstract-logging": "^2.0.1", - "avvio": "^8.2.1", + "avvio": "^8.3.0", "fast-content-type-parse": "^1.1.0", "fast-json-stringify": "^5.8.0", - "find-my-way": "^7.7.0", + "find-my-way": "^8.0.0", "light-my-request": "^5.11.0", "pino": "^8.17.0", "process-warning": "^3.0.0", @@ -4472,21 +4703,21 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -4496,14 +4727,14 @@ } }, "node_modules/find-my-way": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.7.0.tgz", - "integrity": "sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.0.tgz", + "integrity": "sha512-HdWXgFYc6b1BJcOBDBwjqWuHJj1WYiqrxSh25qtU4DabpMFdj/gSunNBQb83t+8Zt67D7CXEzJWTkxaShMTMOA==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", - "safe-regex2": "^2.0.0" + "safe-regex2": "^3.1.0" }, "engines": { "node": ">=14" @@ -4516,96 +4747,38 @@ "dev": true, "dependencies": { "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flat-cache/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/flat-cache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flat-cache/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" + "path-exists": "^4.0.0" }, "engines": { - "node": "*" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -4646,18 +4819,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "devOptional": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -4712,9 +4873,9 @@ "dev": true }, "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "dependencies": { "graceful-fs": "^4.2.0", @@ -4722,7 +4883,7 @@ "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/fs-minipass": { @@ -4760,6 +4921,7 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "graceful-fs": "^4.1.2", @@ -4771,20 +4933,11 @@ "node": ">=0.6" } }, - "node_modules/fstream/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/fstream/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -4801,22 +4954,11 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fstream/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/fstream/node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -4842,9 +4984,9 @@ } }, "node_modules/futoin-hkdf": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.4.3.tgz", - "integrity": "sha512-K4MIe2xSVRMYxsA4w0ap5fp1C2hA9StA2Ad1JZHX57VMCdHIRB5BSrd1FhuadTQG9MkjggaTCrw7v5XXFyY3/w==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz", + "integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==", "dev": true, "engines": { "node": ">=8" @@ -4854,6 +4996,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", "optional": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -4870,15 +5013,42 @@ "node": ">=10" } }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/gaxios": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.2.0.tgz", - "integrity": "sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.6.0.tgz", + "integrity": "sha512-bpOZVQV5gthH/jVCSuYuokRo2bTKOcuBiVWpjmTn6C5Agl5zclGfTljuGsQZxwwDBkli+YhZhP4TdlqTnhOezQ==", "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" }, "engines": { "node": ">=14" @@ -4945,19 +5115,22 @@ "dev": true }, "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dev": true, + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "devOptional": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4975,16 +5148,28 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "devOptional": true, "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/global-dirs": { @@ -5012,15 +5197,12 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.3.0.tgz", + "integrity": "sha512-cCdyVjIUVTtX8ZsPkq1oCsOsLmGIswqnjZYMJJTGaNApj1yHtLSymKhwH51ttirREn75z3p4k051clwg7rvNKA==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5047,9 +5229,9 @@ } }, "node_modules/google-auth-library": { - "version": "9.6.3", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.6.3.tgz", - "integrity": "sha512-4CacM29MLC2eT9Cey5GDVK4Q8t+MMp8+OEdOaqD9MG6b0dOyLORaaeJMPQ7EESVgm/+z5EKYyFLxgzBJlJgyHQ==", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.10.0.tgz", + "integrity": "sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==", "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", @@ -5063,9 +5245,9 @@ } }, "node_modules/googleapis": { - "version": "133.0.0", - "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-133.0.0.tgz", - "integrity": "sha512-6xyc49j+x7N4smawJs/q1i7mbSkt6SYUWWd9RbsmmDW7gRv+mhwZ4xT+XkPihZcNyo/diF//543WZq4szdS74w==", + "version": "137.1.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz", + "integrity": "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==", "dependencies": { "google-auth-library": "^9.0.0", "googleapis-common": "^7.0.0" @@ -5075,13 +5257,13 @@ } }, "node_modules/googleapis-common": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.0.1.tgz", - "integrity": "sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", "dependencies": { "extend": "^3.0.2", "gaxios": "^6.0.3", - "google-auth-library": "^9.0.0", + "google-auth-library": "^9.7.0", "qs": "^6.7.0", "url-template": "^2.0.8", "uuid": "^9.0.0" @@ -5151,24 +5333,24 @@ } }, "node_modules/hap-nodejs": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.11.1.tgz", - "integrity": "sha512-hJuGyjng2jlzhZsviWCldaokT7l7BE3iGmWdlE6DNmQFDTmiBN3deNksAZ2nt7qp5jYEv7ZUvW7WBZqJsLh3ww==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.12.1.tgz", + "integrity": "sha512-iUUMaK6ucDKLMjT4m5Oz6CoLKkGg+omI6GR96weyL8fPGR1HYoCMtoJoUNW2NSIp4b2A6hx4zjNOEtLEaTA2MQ==", "dev": true, "dependencies": { - "@homebridge/ciao": "^1.1.5", - "@homebridge/dbus-native": "^0.5.1", - "bonjour-hap": "~3.6.4", + "@homebridge/ciao": "^1.2.0", + "@homebridge/dbus-native": "^0.6.0", + "bonjour-hap": "^3.7.2", "debug": "^4.3.4", - "fast-srp-hap": "~2.0.4", - "futoin-hkdf": "~1.4.3", + "fast-srp-hap": "^2.0.4", + "futoin-hkdf": "^1.5.3", "node-persist": "^0.0.11", "source-map-support": "^0.5.21", - "tslib": "^2.4.0", + "tslib": "^2.6.2", "tweetnacl": "^1.0.3" }, "engines": { - "node": ">=10.17.0" + "node": "^18 || ^20" } }, "node_modules/has-bigints": { @@ -5183,25 +5365,26 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -5253,9 +5436,9 @@ } }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -5264,13 +5447,13 @@ } }, "node_modules/hb-lib-tools": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/hb-lib-tools/-/hb-lib-tools-1.2.2.tgz", - "integrity": "sha512-xFU9vxH4Ap3GoZ1zRBYGNCGvRz6lFh0ylYx0QaoYKcydVLHQKleSJX9g3X/YmgcQNpfj4XFWT7uDB/XDZeW4Nw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hb-lib-tools/-/hb-lib-tools-2.0.1.tgz", + "integrity": "sha512-okOzIvlR4QgfyCh7qIOczRxJDuv3TdzvWTMFlYK8c4UTSXHnIb/dWBvkud7tQyXa9n8FA2DBTJdAUSWJLM63sA==", "dependencies": { - "bonjour-hap": "^3.6.4", - "chalk": "^4.1.2", - "semver": "^7.5.4" + "bonjour-hap": "^3.7.2", + "chalk": "^5.3.0", + "semver": "^7.6.2" }, "bin": { "hap": "cli/hap.js", @@ -5279,7 +5462,18 @@ "upnp": "cli/upnp.js" }, "engines": { - "node": "20.11.0||^20||^18" + "node": "20.13.1||^20||^18" + } + }, + "node_modules/hb-lib-tools/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/helmet": { @@ -5300,19 +5494,11 @@ "readable-stream": "^3.6.0" } }, - "node_modules/help-me/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/help-me/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5328,39 +5514,31 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/help-me/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/hexy": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.2.11.tgz", - "integrity": "sha512-ciq6hFsSG/Bpt2DmrZJtv+56zpPdnq+NQ4ijEFrveKN0ZG1mhl/LdT1NQZ9se6ty1fACcI4d4vYqC9v8EYpH2A==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.3.5.tgz", + "integrity": "sha512-UCP7TIZPXz5kxYJnNOym+9xaenxCLor/JyhKieo8y8/bJWunGh9xbhy3YrgYJUQ87WwfXGm05X330DszOfINZw==", "dev": true, "bin": { "hexy": "bin/hexy_cmd.js" + }, + "engines": { + "node": ">=10.4" } }, "node_modules/homebridge": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.7.0.tgz", - "integrity": "sha512-2QikXnmpnFe2s33Q8TeYE5+sXyKHUZ+9l5WfDmpuupHdct6H/G6b6z3HCj+2rlMRKKY5ElLv5XtLoxOcafnL0g==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.8.2.tgz", + "integrity": "sha512-K0P9/qk3RdAKGLhGrmtF4skUjcygNlnBu0S/ssKIdp4p0kMzW2wjw2Q+z7TCxgZVy84/kaR09UD1n6uJAunTOQ==", "dev": true, "dependencies": { - "chalk": "^4.1.2", - "commander": "^7.2.0", - "fs-extra": "^10.1.0", - "hap-nodejs": "~0.11.1", - "qrcode-terminal": "^0.12.0", - "semver": "^7.5.4", - "source-map-support": "^0.5.21" + "chalk": "4.1.2", + "commander": "12.0.0", + "fs-extra": "11.2.0", + "hap-nodejs": "0.12.1", + "qrcode-terminal": "0.12.0", + "semver": "7.6.2", + "source-map-support": "0.5.21" }, "bin": { "homebridge": "bin/homebridge" @@ -5370,9 +5548,9 @@ } }, "node_modules/homebridge-config-ui-x": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/homebridge-config-ui-x/-/homebridge-config-ui-x-4.55.1.tgz", - "integrity": "sha512-XojwCOEB6LsJeYt6NFakXGCIs9WFA/xiraVhC1R1+//bQPmv1T1F3sZvrPxp1UTBLKxoxT37SMdtq0us8pRyaw==", + "version": "4.56.2", + "resolved": "https://registry.npmjs.org/homebridge-config-ui-x/-/homebridge-config-ui-x-4.56.2.tgz", + "integrity": "sha512-jsWr/EgxUkxznlQ1UxUjZWYc+xl99tWj6n5qqL7Xpe683qjYv5hA71Bj75v6ETwrz2sPttUb2sXMUIZDNvxtDA==", "dev": true, "funding": [ { @@ -5386,28 +5564,27 @@ ], "dependencies": { "@fastify/helmet": "11.1.1", - "@fastify/multipart": "8.0.0", - "@fastify/static": "6.12.0", + "@fastify/multipart": "8.2.0", + "@fastify/static": "7.0.2", + "@homebridge/hap-client": "1.10.0", "@homebridge/node-pty-prebuilt-multiarch": "0.11.12", - "@nestjs/axios": "3.0.1", - "@nestjs/common": "10.3.0", - "@nestjs/core": "10.3.0", + "@nestjs/axios": "3.0.2", + "@nestjs/common": "10.3.7", + "@nestjs/core": "10.3.7", "@nestjs/jwt": "10.2.0", "@nestjs/passport": "10.0.3", - "@nestjs/platform-fastify": "10.3.0", - "@nestjs/platform-socket.io": "10.3.0", - "@nestjs/swagger": "7.1.17", - "@nestjs/websockets": "10.3.0", - "@oznu/hap-client": "1.9.0", - "axios": "1.6.5", + "@nestjs/platform-fastify": "10.3.7", + "@nestjs/platform-socket.io": "10.3.7", + "@nestjs/swagger": "7.3.1", + "@nestjs/websockets": "10.3.7", + "axios": "1.6.8", "bash-color": "0.0.4", - "bonjour-service": "=1.1.1", "buffer-shims": "1.0.0", "class-transformer": "0.5.1", - "class-validator": "0.14.0", - "commander": "11.1.0", + "class-validator": "0.14.1", + "commander": "12.0.0", "dayjs": "1.11.10", - "fastify": "4.25.1", + "fastify": "4.26.2", "fs-extra": "11.2.0", "jsonwebtoken": "9.0.2", "lodash": "4.17.21", @@ -5418,12 +5595,12 @@ "p-limit": "3.1.0", "passport": "0.7.0", "passport-jwt": "4.0.1", - "reflect-metadata": "0.1.14", + "reflect-metadata": "0.2.2", "rxjs": "7.8.1", - "semver": "7.5.4", - "systeminformation": "5.21.22", + "semver": "7.6.0", + "systeminformation": "5.22.7", "tail": "2.2.6", - "tar": "6.2.0", + "tar": "6.2.1", "tcp-port-used": "1.0.2", "unzipper": "0.10.14" }, @@ -5436,33 +5613,10 @@ "node": "^18 || ^20" } }, - "node_modules/homebridge-config-ui-x/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/homebridge-config-ui-x/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/homebridge-config-ui-x/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -5475,12 +5629,12 @@ } }, "node_modules/homebridge-lib": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/homebridge-lib/-/homebridge-lib-6.7.3.tgz", - "integrity": "sha512-1G2FR3T+/GpRPYD2JU4dJWgf2OFbX/5dzx8doyVS9htkSbZWN4l+rXSVVU2K2KBidnj3NNOVBj21uw/20XGOgQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/homebridge-lib/-/homebridge-lib-7.0.1.tgz", + "integrity": "sha512-OMwQEx1Uhb67VsFoY+s9KXUjc6g7XQP86GrNEvPD7ldnaA/D0HN3KHfsaJ0JyZaNCvUOEqG8A4youCMj73RSSw==", "dependencies": { - "@homebridge/plugin-ui-utils": "~1.0.0", - "hb-lib-tools": "~1.2.1" + "@homebridge/plugin-ui-utils": "~1.0.3", + "hb-lib-tools": "~2.0.1" }, "bin": { "hap": "cli/hap.js", @@ -5489,8 +5643,8 @@ "upnp": "cli/upnp.js" }, "engines": { - "homebridge": "^1.7.0", - "node": "20.10.0||^20||^18" + "homebridge": "^1.8.1", + "node": "20.13.1||^20||^18" } }, "node_modules/hosted-git-info": { @@ -5537,9 +5691,9 @@ } }, "node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "optional": true, "dependencies": { "agent-base": "^7.1.0", @@ -5563,9 +5717,9 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -5630,9 +5784,9 @@ "dev": true }, "node_modules/ignore-walk": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.4.tgz", - "integrity": "sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", "dev": true, "dependencies": { "minimatch": "^9.0.0" @@ -5641,6 +5795,30 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -5691,18 +5869,19 @@ "dev": true }, "node_modules/inflection": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", - "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.0.tgz", + "integrity": "sha512-1zEJU1l19SgJlmwqsEyFTbScw/tkMHFenUo//Y0i+XEP83gDFdMvPizAD/WGcE+l1ku12PcTVHQhO6g5E0UCMw==", "dev": true, - "engines": [ - "node >= 0.4.0" - ] + "engines": { + "node": ">=18.0.0" + } }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -5714,9 +5893,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -5735,11 +5914,6 @@ "node": ">= 0.4" } }, - "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" - }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -5950,9 +6124,12 @@ "devOptional": true }, "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6026,19 +6203,25 @@ } }, "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6108,20 +6291,26 @@ "dev": true }, "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6171,9 +6360,9 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", + "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", "devOptional": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -6236,9 +6425,9 @@ "dev": true }, "node_modules/json-parse-even-better-errors": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", - "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -6259,24 +6448,7 @@ "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.3" - } - }, - "node_modules/json-schema-resolver": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-2.0.0.tgz", - "integrity": "sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "rfdc": "^1.1.4", - "uri-js": "^4.2.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/Eomm/json-schema-resolver?sponsor=1" + "fast-deep-equal": "^3.1.3" } }, "node_modules/json-schema-traverse": { @@ -6447,28 +6619,22 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.10.55", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.55.tgz", - "integrity": "sha512-MrTg2JFLscgmTY6/oT9vopYETlgUls/FU6OaeeamGwk4LFxjIgOUML/ZSZICgR0LPYXaonVJo40lzMvaaTJlQA==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.2.tgz", + "integrity": "sha512-V9mGLlaXN1WETzqQvSu6qf6XVAr3nFuJvWsHcuzCCCo6xUKawwSxOPTpan5CGOSKTn5w/bQuCZcLPJkyysgC3w==", "dev": true }, "node_modules/light-my-request": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.0.tgz", - "integrity": "sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.12.0.tgz", + "integrity": "sha512-P526OX6E7aeCIfw/9UyJNsAISfcFETghysaWHQAlQYayynShT08MOj4c6fBCvTWBrHXSvqBAKDp3amUPSCQI4w==", "dev": true, "dependencies": { - "cookie": "^0.5.0", - "process-warning": "^2.0.0", + "cookie": "^0.6.0", + "process-warning": "^3.0.0", "set-cookie-parser": "^2.4.1" } }, - "node_modules/light-my-request/node_modules/process-warning": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", - "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==", - "dev": true - }, "node_modules/listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", @@ -6629,9 +6795,9 @@ "dev": true }, "node_modules/make-fetch-happen": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz", - "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", "optional": true, "dependencies": { "@npmcli/agent": "^2.0.0", @@ -6643,6 +6809,7 @@ "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", + "proc-log": "^4.2.0", "promise-retry": "^2.0.1", "ssri": "^10.0.0" }, @@ -6650,6 +6817,15 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/make-fetch-happen/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "optional": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/map-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", @@ -6666,18 +6842,30 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -6733,18 +6921,14 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "devOptional": true, + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { @@ -6756,9 +6940,9 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "devOptional": true, "engines": { "node": ">=16 || 14 >=14.17" @@ -6777,9 +6961,9 @@ } }, "node_modules/minipass-fetch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", - "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", "devOptional": true, "dependencies": { "minipass": "^7.0.3", @@ -6931,9 +7115,9 @@ "dev": true }, "node_modules/mnemonist": { - "version": "0.39.5", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.5.tgz", - "integrity": "sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==", + "version": "0.39.6", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz", + "integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==", "dev": true, "dependencies": { "obliterator": "^2.0.1" @@ -7004,9 +7188,9 @@ "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==" }, "node_modules/nan": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", - "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", "devOptional": true }, "node_modules/napi-build-utils": { @@ -7037,9 +7221,9 @@ } }, "node_modules/node-abi": { - "version": "3.54.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.54.0.tgz", - "integrity": "sha512-p7eGEiQil0YUV3ItH4/tBb781L5impVmmx2E9FRKF7d18XXzp4PGT2tdYMFY6wQqgxD0IwNZOiSJ0/K0fSi/OA==", + "version": "3.62.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz", + "integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==", "dev": true, "dependencies": { "semver": "^7.3.5" @@ -7086,9 +7270,9 @@ } }, "node_modules/node-gyp": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.0.1.tgz", - "integrity": "sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", + "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", "optional": true, "dependencies": { "env-paths": "^2.2.0", @@ -7110,9 +7294,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", - "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", "optional": true, "bin": { "node-gyp-build": "bin.js", @@ -7129,28 +7313,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/node-gyp/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "optional": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/node-gyp/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -7161,9 +7323,9 @@ } }, "node_modules/node-gyp/node_modules/nopt": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", - "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", "optional": true, "dependencies": { "abbrev": "^2.0.0" @@ -7215,21 +7377,21 @@ } }, "node_modules/node-switchbot": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/node-switchbot/-/node-switchbot-2.0.3.tgz", - "integrity": "sha512-fPmtTN/ba1fG6uW/6Witdju1i9C8WJgUYxR0hfeF09A0HZ6MnNLV01et3kVyCcXyUsdyjthp7qms/h4lFq0mmA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/node-switchbot/-/node-switchbot-2.1.1.tgz", + "integrity": "sha512-QmjlroUt721Ik8Bq8AhqBqMMZgqXNF4jdbuGRpbLFOUcRXRmrecBVLlbhvXy++4FLMWaCzAuXMnpJ1qxv+IYDw==", "optional": true, "dependencies": { - "@abandonware/noble": "^1.9.2-24" + "@abandonware/noble": "^1.9.2-25" }, "optionalDependencies": { - "@abandonware/bluetooth-hci-socket": "^0.5.3-11" + "@abandonware/bluetooth-hci-socket": "^0.5.3-12" } }, "node_modules/nodemon": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.3.tgz", - "integrity": "sha512-7jH/NXbFPxVaMwmBCC2B9F/V6X1VkEdNgx3iu9jji8WxWcvhMWkmhNWhI5077zknOnZnBzba9hZP6bCPJLSReQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.1.tgz", + "integrity": "sha512-k43xGaDtaDIcufn0Fc6fTtsdKSkV/hQzoQFigNH//GaKta28yoKVYXCnV+KXRqfT/YzsFaQU9VdeEG+HEyxr6A==", "dev": true, "dependencies": { "chokidar": "^3.5.2", @@ -7254,16 +7416,6 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/nodemon/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -7273,18 +7425,6 @@ "node": ">=4" } }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/nodemon/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -7358,9 +7498,9 @@ } }, "node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", "dev": true, "engines": { "node": ">=14.16" @@ -7370,9 +7510,9 @@ } }, "node_modules/npm-bundled": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", - "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", "dev": true, "dependencies": { "npm-normalize-package-bin": "^3.0.0" @@ -7382,11 +7522,12 @@ } }, "node_modules/npm-check-updates": { - "version": "16.14.15", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.15.tgz", - "integrity": "sha512-WH0wJ9j6CP7Azl+LLCxWAYqroT2IX02kRIzgK/fg0rPpMbETgHITWBdOPtrv521xmA3JMgeNsQ62zvVtS/nCmQ==", + "version": "16.14.20", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.20.tgz", + "integrity": "sha512-sYbIhun4DrjO7NFOTdvs11nCar0etEhZTsEjL47eM0TuiGMhmYughRCxG2SpGRmGAQ7AkwN7bw2lWzoE7q6yOQ==", "dev": true, "dependencies": { + "@types/semver-utils": "^1.1.1", "chalk": "^5.3.0", "cli-table3": "^0.6.3", "commander": "^10.0.1", @@ -7452,6 +7593,15 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/npm-check-updates/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/npm-check-updates/node_modules/cacache": { "version": "17.1.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", @@ -7496,28 +7646,6 @@ "node": ">=14" } }, - "node_modules/npm-check-updates/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/npm-check-updates/node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -7589,6 +7717,21 @@ "node": ">=8" } }, + "node_modules/npm-check-updates/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/npm-check-updates/node_modules/minipass-collect": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", @@ -7792,34 +7935,12 @@ } }, "node_modules/npm-registry-fetch/node_modules/cacache/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm-registry-fetch/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, "engines": { "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" } }, "node_modules/npm-registry-fetch/node_modules/http-proxy-agent": { @@ -7935,6 +8056,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", "optional": true, "dependencies": { "are-we-there-yet": "^2.0.0", @@ -7979,12 +8101,12 @@ } }, "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -8056,24 +8178,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "dev": true - }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -8241,34 +8357,12 @@ } }, "node_modules/pacote/node_modules/cacache/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/pacote/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, "engines": { "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" } }, "node_modules/pacote/node_modules/lru-cache": { @@ -8401,25 +8495,25 @@ } }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "devOptional": true, "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", "devOptional": true, "engines": { "node": "14 || >=16.14" @@ -8456,43 +8550,43 @@ } }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pino": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.18.0.tgz", - "integrity": "sha512-Mz/gKiRyuXu4HnpHgi1YWdHQCoWMufapzooisvFn78zl4dZciAxS+YeRkUxXl1ee/SzU80YCz1zpECCh4oC6Aw==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", "dev": true, "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "v1.1.0", + "pino-abstract-transport": "^1.2.0", "pino-std-serializers": "^6.0.0", "process-warning": "^3.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^3.7.0", - "thread-stream": "^2.0.0" + "thread-stream": "^2.6.0" }, "bin": { "pino": "bin.js" } }, "node_modules/pino-abstract-transport": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", - "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", "dev": true, "dependencies": { "readable-stream": "^4.0.0", @@ -8554,10 +8648,18 @@ "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", "dev": true }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", "dev": true, "dependencies": { "detect-libc": "^2.0.0", @@ -8743,11 +8845,11 @@ } }, "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -8840,6 +8942,7 @@ "version": "6.0.4", "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", "dev": true, "dependencies": { "glob": "^10.2.2", @@ -8864,28 +8967,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -8911,6 +8992,18 @@ "node": ">=8.10.0" } }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -8921,15 +9014,9 @@ } }, "node_modules/reflect-metadata": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", - "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", - "dev": true - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true }, "node_modules/regexp.prototype.flags": { @@ -9042,13 +9129,19 @@ "node": ">=8" } }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/ret": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", - "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", "dev": true, "engines": { - "node": ">=4" + "node": ">=10" } }, "node_modules/retry": { @@ -9076,9 +9169,9 @@ "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" }, "node_modules/rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", + "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==", "dev": true, "dependencies": { "glob": "^10.3.7" @@ -9087,29 +9180,7 @@ "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -9166,12 +9237,12 @@ ] }, "node_modules/safe-regex2": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", - "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", "dev": true, "dependencies": { - "ret": "~0.2.0" + "ret": "~0.4.0" } }, "node_modules/safe-stable-stringify": { @@ -9202,12 +9273,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -9249,29 +9317,30 @@ "dev": true }, "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.2", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -9311,11 +9380,11 @@ } }, "node_modules/side-channel": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", - "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" @@ -9328,10 +9397,16 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/sigstore": { "version": "1.9.0", @@ -9387,28 +9462,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/sigstore/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/sigstore/node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -9601,9 +9654,9 @@ } }, "node_modules/socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", "dev": true, "dependencies": { "accepts": "~1.3.4", @@ -9619,11 +9672,12 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", "dev": true, "dependencies": { + "debug": "~4.3.4", "ws": "~8.11.0" } }, @@ -9662,9 +9716,9 @@ } }, "node_modules/socks": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.3.tgz", - "integrity": "sha512-vfuYK48HXCTFD03G/1/zkIls3Ebr2YNa4qU9gHDZdblHLiqhJrJGkY3+0Nx0JpN9qBhJbVObc1CNciT1bIZJxw==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "devOptional": true, "dependencies": { "ip-address": "^9.0.5", @@ -9676,12 +9730,12 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", - "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", "optional": true, "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.1", "debug": "^4.3.4", "socks": "^2.7.1" }, @@ -9690,9 +9744,9 @@ } }, "node_modules/sonic-boom": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", - "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", "dev": true, "dependencies": { "atomic-sleep": "^1.0.0" @@ -9746,9 +9800,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", - "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -9762,9 +9816,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", "dev": true }, "node_modules/split": { @@ -9794,9 +9848,9 @@ "devOptional": true }, "node_modules/ssri": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", - "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", "devOptional": true, "dependencies": { "minipass": "^7.0.3" @@ -9858,17 +9912,20 @@ } }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "devOptional": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width-cjs": { @@ -9886,6 +9943,39 @@ "node": ">=8" } }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "devOptional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -9927,6 +10017,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -9935,15 +10026,15 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.10.3", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.10.3.tgz", - "integrity": "sha512-fu3aozjxFWsmcO1vyt1q1Ji2kN7KlTd1vHy27E9WgPyXo9nrEzhQPqgxaAjbMsOmb8XFKNGo4Sa3Q+84Fh+pFw==", + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz", + "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==", "dev": true }, "node_modules/systeminformation": { - "version": "5.21.22", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.22.tgz", - "integrity": "sha512-gNHloAJSyS+sKWkwvmvozZ1eHrdVTEsynWMTY6lvLGBB70gflkBQFw8drXXr1oEXY84+Vr9tOOrN8xHZLJSycA==", + "version": "5.22.7", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.7.tgz", + "integrity": "sha512-AWxlP05KeHbpGdgvZkcudJpsmChc2Y5Eo/GvxG/iUA/Aws5LZKHAMSeAo+V+nD+nxWZaxrwpWcnx4SH3oxNL3A==", "dev": true, "os": [ "darwin", @@ -9976,9 +10067,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "devOptional": true, "dependencies": { "chownr": "^2.0.0", @@ -10098,12 +10189,6 @@ } } }, - "node_modules/text-decoding": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", - "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==", - "dev": true - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -10120,9 +10205,9 @@ } }, "node_modules/thread-stream": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", - "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", "dev": true, "dependencies": { "real-require": "^0.2.0" @@ -10170,32 +10255,14 @@ } }, "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", "dev": true, - "dependencies": { - "nopt": "~1.0.10" - }, "bin": { "nodetouch": "bin/nodetouch.js" } }, - "node_modules/touch/node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -10211,9 +10278,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", - "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { "node": ">=16" @@ -10319,28 +10386,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/tuf-js/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/tuf-js/node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -10481,12 +10526,12 @@ } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10507,9 +10552,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -10519,6 +10564,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.0.0-alpha.16.tgz", + "integrity": "sha512-hseQjFKLOZXuBjGgEoYWKD+EL1yd2nVvqL9TLq8RELE1ZGkha15WS98GfwpREZkak+CuTPNsRHHNxeXUesQ/DA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.0.0-alpha.16", + "@typescript-eslint/parser": "8.0.0-alpha.16", + "@typescript-eslint/utils": "8.0.0-alpha.16" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", @@ -10538,14 +10606,11 @@ "dev": true }, "node_modules/undici": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.6.2.tgz", - "integrity": "sha512-vSqvUE5skSxQJ5sztTZ/CdeJb1Wq0Hf44hlYMciqHghvz+K88U0l7D6u1VsndoFgskDcnU+nG3gYmMzJVzd9Qg==", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.18.1.tgz", + "integrity": "sha512-/0BWqR8rJNRysS5lqVmfc7eeOErcOP4tZpATVjJOojjHZ71gSYVAtFhEmadcIjwMIUehh5NFyKGsXCnXIajtbA==", "engines": { - "node": ">=18.0" + "node": ">=18.17" } }, "node_modules/undici-types": { @@ -10554,14 +10619,6 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, - "node_modules/undici/node_modules/@fastify/busboy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", - "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", - "engines": { - "node": ">=14" - } - }, "node_modules/unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", @@ -10728,14 +10785,14 @@ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" }, "node_modules/usb": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/usb/-/usb-2.11.0.tgz", - "integrity": "sha512-u5+NZ6DtoW8TIBtuSArQGAZZ/K15i3lYvZBAYmcgI+RcDS9G50/KPrUd3CrU8M92ahyCvg5e0gc8BDvr5Hwejg==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.13.0.tgz", + "integrity": "sha512-pTNKyxD1DfC1DYu8kFcIdpE8f33e0c2Sbmmi0HEs28HTVC555uocvYR1g5DDv4CBibacCh4BqRyYZJylN4mBbw==", "hasInstallScript": true, "optional": true, "dependencies": { "@types/w3c-web-usb": "^1.0.6", - "node-addon-api": "^7.0.0", + "node-addon-api": "^8.0.0", "node-gyp-build": "^4.5.0" }, "engines": { @@ -10743,12 +10800,12 @@ } }, "node_modules/usb/node_modules/node-addon-api": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", - "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.0.0.tgz", + "integrity": "sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==", "optional": true, "engines": { - "node": "^16 || ^18 || >= 20" + "node": "^18 || ^20 || >= 21" } }, "node_modules/util-deprecate": { @@ -10794,21 +10851,18 @@ } }, "node_modules/validate-npm-package-name": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", - "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", "dev": true, - "dependencies": { - "builtins": "^5.0.0" - }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", "dev": true, "engines": { "node": ">= 0.10" @@ -10877,29 +10931,32 @@ } }, "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-typed-array": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", - "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dependencies": { - "available-typed-arrays": "^1.0.6", - "call-bind": "^1.0.5", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.1" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -10917,48 +10974,33 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "node_modules/widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", - "dev": true, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, "dependencies": { - "string-width": "^5.0.1" - }, - "engines": { - "node": ">=12" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=8" } }, - "node_modules/widest-line/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/widest-line/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", "dev": true, "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "string-width": "^5.0.1" }, "engines": { "node": ">=12" @@ -10967,19 +11009,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/widest-line/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=0.10.0" } }, "node_modules/wrap-ansi": { @@ -11017,6 +11053,26 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -11041,29 +11097,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "devOptional": true - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "devOptional": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -11096,6 +11129,12 @@ "typedarray-to-buffer": "^3.1.5" } }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/ws": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", @@ -11129,9 +11168,9 @@ } }, "node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "dev": true, "dependencies": { "sax": ">=0.6.0", @@ -11150,15 +11189,6 @@ "node": ">=4.0" } }, - "node_modules/xregexp": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.4.1.tgz", - "integrity": "sha512-2u9HwfadaJaY9zHtRRnH6BY6CQVNQKkYm3oLtC9gJXXzfsbACg5X5e4EZZGVAH+YIfa+QA9lsFQTTe3HURF3ag==", - "dev": true, - "dependencies": { - "@babel/runtime-corejs3": "^7.12.1" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -11172,15 +11202,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index f2cc11c5..a6665aad 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "displayName": "SwitchBot", "name": "@switchbot/homebridge-switchbot", - "version": "3.4.0", + "version": "3.5.0", "description": "The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.", "author": { "name": "SwitchBot", @@ -24,9 +24,10 @@ "bugs": { "url": "https://github.com/OpenWonderLabs/homebridge-switchbot/issues" }, + "engineStrict": true, "engines": { - "homebridge": "^1.7.0", - "node": "^18 || ^20" + "homebridge": "^1.8.2", + "node": "^18 || ^20 || ^22" }, "main": "dist/index.js", "scripts": { @@ -76,27 +77,30 @@ "ir" ], "dependencies": { - "@homebridge/plugin-ui-utils": "^1.0.1", + "@homebridge/plugin-ui-utils": "^1.0.3", "async-mqtt": "^2.6.3", "fakegato-history": "^0.6.4", - "homebridge-lib": "^6.7.3", + "homebridge-lib": "^7.0.1", "rxjs": "^7.8.1", - "undici": "^6.6.2" + "undici": "^6.18.1" }, "optionalDependencies": { - "node-switchbot": "2.0.3" + "node-switchbot": "2.1.1" }, "devDependencies": { - "@types/node": "^20.11.17", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint": "^8.56.0", - "homebridge": "^1.7.0", - "homebridge-config-ui-x": "4.55.1", - "nodemon": "^3.0.3", - "npm-check-updates": "^16.14.15", - "rimraf": "^5.0.5", + "@eslint/js": "^9.3.0", + "@stylistic/eslint-plugin": "^2.1.0", + "@types/eslint__js": "^8.42.3", + "@types/node": "^20.12.12", + "eslint": "^9.3.0", + "globals": "^15.3.0", + "homebridge": "^1.8.2", + "homebridge-config-ui-x": "4.56.2", + "nodemon": "^3.1.1", + "npm-check-updates": "^16.14.20", + "rimraf": "^5.0.7", "ts-node": "^10.9.2", - "typescript": "^5.3.3" + "typescript": "^5.4.5", + "typescript-eslint": "^8.0.0-alpha.14" } } diff --git a/src/custom.d.ts b/src/custom.d.ts index eea79da3..49926c56 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -1,6 +1,7 @@ -/* Copyright(C) 2017-2023, donavanbecker (https://github.com/donavanbecker). All rights reserved. +/* Copyright(C) 2017-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. * * custom.d.ts: @switchbot/homebridge-switchbot platform class. */ declare module 'homebridge-lib' +declare module 'homebridge-lib/EveHomeKitTypes' declare module 'fakegato-history' diff --git a/src/device/blindtilt.ts b/src/device/blindtilt.ts index d9c74334..fbdbda3e 100644 --- a/src/device/blindtilt.ts +++ b/src/device/blindtilt.ts @@ -1,113 +1,63 @@ +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * blindtilt.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { sleep } from '../utils.js'; -import { MqttClient } from 'mqtt'; +import { Devices } from '../settings.js'; import { interval, Subject } from 'rxjs'; -import asyncmqtt from 'async-mqtt'; -import { SwitchBotPlatform } from '../platform.js'; +import { deviceBase } from './device.js'; +import { BlindTiltMappingMode } from '../utils.js'; import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { Service, PlatformAccessory, CharacteristicValue, Logging, API, HAP } from 'homebridge'; -import { device, devicesConfig, serviceData, deviceStatus, Devices, SwitchBotPlatformConfig } from '../settings.js'; - -enum BlindTiltMappingMode { - OnlyUp = 'only_up', - OnlyDown = 'only_down', - DownAndUp = 'down_and_up', - UpAndDown = 'up_and_down', - UseTiltForDirection = 'use_tilt_for_direction', -} -export class BlindTilt { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +import type { SwitchBotPlatform } from '../platform.js'; +import type { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; + + +export class BlindTilt extends deviceBase { // Services - batteryService!: Service; - lightSensorService?: Service; - windowCoveringService!: Service; - - // Characteristic Values - BatteryLevel!: CharacteristicValue; - PositionState!: CharacteristicValue; - TargetPosition!: CharacteristicValue; - CurrentPosition!: CharacteristicValue; - StatusLowBattery!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - CurrentAmbientLightLevel?: CharacteristicValue; - TargetHorizontalTiltAngle!: CharacteristicValue; - CurrentHorizontalTiltAngle!: CharacteristicValue; - - // OpenAPI Status - OpenAPI_InMotion: deviceStatus['moving']; - OpenAPI_BatteryLevel: serviceData['battery']; - OpenAPI_Direction: deviceStatus['direction']; - OpenAPI_Calibration: serviceData['calibration']; - OpenAPI_CurrentPosition: serviceData['position']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_CurrentAmbientLightLevel: deviceStatus['brightness']; + private WindowCovering: { + Name: CharacteristicValue; + Service: Service; + PositionState: CharacteristicValue; + TargetPosition: CharacteristicValue; + CurrentPosition: CharacteristicValue; + TargetHorizontalTiltAngle: CharacteristicValue; + CurrentHorizontalTiltAngle: CharacteristicValue; + }; + + private Battery: { + Name: CharacteristicValue; + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + ChargingState?: CharacteristicValue; + }; + + private LightSensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentAmbientLightLevel?: CharacteristicValue; + }; // OpenAPI Others - Mode!: string; - setPositionMode?: string | number; mappingMode: BlindTiltMappingMode = BlindTiltMappingMode.OnlyUp; - // BLE Status - BLE_InMotion: serviceData['inMotion']; - BLE_BatteryLevel: serviceData['battery']; - BLE_Calibration: serviceData['calibration']; - BLE_CurrentPosition: serviceData['position']; - BLE_CurrentAmbientLightLevel: serviceData['lightLevel']; - - // BLE Others - BLE_IsConnected?: boolean; - spaceBetweenLevels!: number; - // Target setNewTarget!: boolean; setNewTargetTimer!: NodeJS.Timeout; - //MQTT stuff - mqttClient: MqttClient | null = null; - - // Config - set_minStep!: number; - updateRate!: number; - set_minLux!: number; - set_maxLux!: number; - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; - setCloseMode!: string; - setOpenMode!: string; - // Updates blindTiltUpdateInProgress!: boolean; doBlindTiltUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; + super(platform, accessory, device); // default placeholders - this.deviceLogs(device); - this.refreshRate(device); - this.scan(device); - this.setupMqtt(device); - this.deviceContext(); - this.deviceConfig(device); - this.mappingMode = (device.blindTilt?.mode as BlindTiltMappingMode) ?? BlindTiltMappingMode.OnlyUp; this.debugLog(`Mapping mode: ${this.mappingMode}`); @@ -116,57 +66,49 @@ export class BlindTilt { this.blindTiltUpdateInProgress = false; this.setNewTarget = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, 'W2701600') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the WindowCovering service if it exists, otherwise create a new WindowCovering service - // you can create multiple services for each accessory - const windowCoveringService = `${device.deviceName} ${device.deviceType}`; - (this.windowCoveringService = - accessory.getService(this.hap.Service.WindowCovering) - || accessory.addService(this.hap.Service.WindowCovering)), windowCoveringService; - - this.windowCoveringService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.windowCoveringService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.windowCoveringService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - - // create handlers for required characteristics - this.windowCoveringService.setCharacteristic(this.hap.Characteristic.PositionState, this.PositionState); - - this.windowCoveringService - .getCharacteristic(this.hap.Characteristic.CurrentPosition) + // Initialize WindowCovering Service + accessory.context.WindowCovering = accessory.context.WindowCovering ?? {}; + this.WindowCovering = { + Name: accessory.context.WindowCovering.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.WindowCovering) ?? accessory.addService(this.hap.Service.WindowCovering) as Service, + PositionState: accessory.context.PositionState ?? this.hap.Characteristic.PositionState.STOPPED, + TargetPosition: accessory.context.TargetPosition ?? 100, + CurrentPosition: accessory.context.CurrentPosition ?? 100, + TargetHorizontalTiltAngle: accessory.context.TargetHorizontalTiltAngle ?? 90, + CurrentHorizontalTiltAngle: accessory.context.CurrentHorizontalTiltAngle ?? 90, + }; + accessory.context.WindowCovering = this.WindowCovering as object; + + // Initialize WindowCovering Characteristics + this.WindowCovering.Service + .setCharacteristic(this.hap.Characteristic.Name, this.WindowCovering.Name) + .getCharacteristic(this.hap.Characteristic.TargetPosition) .setProps({ - minStep: this.minStep(device), + minStep: device.blindTilt?.set_minStep ?? 1, minValue: 0, maxValue: 100, validValueRanges: [0, 100], }) .onGet(() => { - return this.CurrentPosition; - }); + return this.WindowCovering.TargetPosition; + }) + .onSet(this.TargetPositionSet.bind(this)); - this.windowCoveringService - .getCharacteristic(this.hap.Characteristic.TargetPosition) + // Initialize WindowCovering CurrentPosition Characteristic + this.WindowCovering.Service + .getCharacteristic(this.hap.Characteristic.CurrentPosition) .setProps({ - minStep: this.minStep(device), + minStep: device.blindTilt?.set_minStep ?? 1, minValue: 0, maxValue: 100, validValueRanges: [0, 100], - }) - .onSet(this.TargetPositionSet.bind(this)); + }).onGet(() => { + return this.WindowCovering?.CurrentPosition ?? 0; + }); - this.CurrentHorizontalTiltAngle = 90; - this.windowCoveringService - .getCharacteristic(this.hap.Characteristic.CurrentHorizontalTiltAngle) + // Initialize WindowCovering TargetHorizontalTiltAngle Characteristic + this.WindowCovering.Service + .getCharacteristic(this.hap.Characteristic.TargetHorizontalTiltAngle) .setProps({ minStep: 180, minValue: -90, @@ -174,31 +116,68 @@ export class BlindTilt { validValues: [-90, 90], }) .onGet(() => { - // this.debugLog(`requested CurrentHorizontalTiltAngle: ${this.CurrentHorizontalTiltAngle}`); - return this.CurrentHorizontalTiltAngle; - }); + return this.WindowCovering.TargetHorizontalTiltAngle; + }) + .onSet(this.TargetHorizontalTiltAngleSet.bind(this)); - this.TargetHorizontalTiltAngle = 90; - this.windowCoveringService - .getCharacteristic(this.hap.Characteristic.TargetHorizontalTiltAngle) + // Initialize WindowCovering CurrentHorizontalTiltAngle Characteristic + this.WindowCovering.Service + .getCharacteristic(this.hap.Characteristic.CurrentHorizontalTiltAngle) .setProps({ minStep: 180, minValue: -90, maxValue: 90, validValues: [-90, 90], - }) - .onSet(this.TargetHorizontalTiltAngleSet.bind(this)); - - // Battery Service - const batteryService = `${accessory.displayName} Battery`; - (this.batteryService = this.accessory.getService(this.hap.Service.Battery) - || accessory.addService(this.hap.Service.Battery)), batteryService; + }).onGet(() => { + return this.WindowCovering.CurrentHorizontalTiltAngle ?? 0; + }); - this.batteryService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Battery`); - if (!this.batteryService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.batteryService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Battery`); + // Initialize Battery Service + accessory.context.Battery = accessory.context.Battery ?? {}; + this.Battery = { + Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + ChargingState: accessory.context.ChargingState ?? this.hap.Characteristic.ChargingState.NOT_CHARGING, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Name Characteristic + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE); + + // Initialize LightSensor Service + if (device.blindTilt?.hide_lightsensor) { + if (this.LightSensor?.Service) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} Removing Light Sensor Service`); + this.LightSensor.Service = accessory.getService(this.hap.Service.LightSensor) as Service; + accessory.removeService(this.LightSensor.Service); + accessory.context.LightSensor = {}; + } + } else { + accessory.context.LightSensor = accessory.context.LightSensor ?? {}; + this.LightSensor = { + Name: accessory.context.LightSensor.Name ?? `${accessory.displayName} Light Sensor`, + Service: accessory.getService(this.hap.Service.LightSensor) ?? accessory.addService(this.hap.Service.LightSensor) as Service, + CurrentAmbientLightLevel: accessory.context.CurrentAmbientLightLevel ?? 0.0001, + }; + accessory.context.LightSensor = this.LightSensor as object; + + // Initialize LightSensor Characteristics + this.LightSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LightSensor.Name) + .setCharacteristic(this.hap.Characteristic.StatusActive, true) + .getCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel) + .onGet(() => { + return this.LightSensor?.CurrentAmbientLightLevel ?? 0.0001; + }); } + // Retrieve initial values and updateHomekit + this.refreshStatus(); + // Update Homekit this.updateHomeKitCharacteristics(); @@ -210,36 +189,16 @@ export class BlindTilt { }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Please Submit Logs: ` + 'https://tinyurl.com/SwitchBotBug'); - /*const { temperature, humidity } = context; - const { CurrentTemperature, CurrentRelativeHumidity } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(temperature, humidity) = ' + - `Webhook:(${temperature}, ${humidity}), ` + - `current:(${CurrentTemperature}, ${CurrentRelativeHumidity})`); - this.CurrentRelativeHumidity = humidity; - this.CurrentTemperature = temperature; - this.updateHomeKitCharacteristics();*/ - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); // update slide progress - interval(this.updateRate * 1000) - //.pipe(skipWhile(() => this.blindTiltUpdateInProgress)) + interval(this.deviceUpdateRate * 1000) .subscribe(async () => { - if (this.PositionState === this.hap.Characteristic.PositionState.STOPPED) { + if (this.WindowCovering.PositionState === this.hap.Characteristic.PositionState.STOPPED) { return; } - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Refresh Status When Moving, PositionState: ${this.PositionState}`); + this.debugLog(`${device.deviceType}: ${accessory.displayName} Refresh Status When Moving,` + + ` PositionState: ${this.WindowCovering.PositionState}`); await this.refreshStatus(); }); @@ -250,235 +209,241 @@ export class BlindTilt { tap(() => { this.blindTiltUpdateInProgress = true; }), - debounceTime(this.platform.config.options!.pushRate! * 1000), + debounceTime(this.devicePushRate * 1000), ) .subscribe(async () => { try { await this.pushChanges(); } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + - ` Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed pushChanges with ${device.connectionType} Connection,` + + ` Error Message: ${JSON.stringify(e.message)}`); } this.blindTiltUpdateInProgress = false; }); } /** - * Parse the device status from the SwitchBot api + * Parse the device status from the SwitchBotBLE API */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // CurrentPosition - this.CurrentPosition = 100 - Number(this.BLE_CurrentPosition); + this.WindowCovering.CurrentPosition = 100 - Number(serviceData.position); await this.setMinMax(); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition ${this.CurrentPosition}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition ${this.WindowCovering.CurrentPosition}`); if (this.setNewTarget) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Checking Status ...`); } - if (this.setNewTarget && this.BLE_InMotion) { + if (this.setNewTarget && serviceData.inMotion) { await this.setMinMax(); - if (Number(this.TargetPosition) > this.CurrentPosition) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Closing, CurrentPosition: ${this.CurrentPosition}`); - this.PositionState = this.hap.Characteristic.PositionState.INCREASING; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} INCREASING PositionState: ${this.PositionState}`); - } else if (Number(this.TargetPosition) < this.CurrentPosition) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Opening, CurrentPosition: ${this.CurrentPosition}`); - this.PositionState = this.hap.Characteristic.PositionState.DECREASING; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} DECREASING PositionState: ${this.PositionState}`); + if (Number(this.WindowCovering.TargetPosition) > this.WindowCovering.CurrentPosition) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Closing, CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.INCREASING; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} INCREASING` + + ` PositionState: ${this.WindowCovering.PositionState}`); + } else if (Number(this.WindowCovering.TargetPosition) < this.WindowCovering.CurrentPosition) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Opening, CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.DECREASING; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} DECREASING` + + ` PositionState: ${this.WindowCovering.PositionState}`); } else { - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} Standby2, CurrentPosition: ${this.CurrentPosition}`); - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} STOPPED PositionState: ${this.PositionState}`); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} Standby2,` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.STOPPED; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} STOPPED` + + ` PositionState: ${this.WindowCovering.PositionState}`); } } else { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Standby, CurrentPosition: ${this.CurrentPosition}`); - this.TargetPosition = this.CurrentPosition; - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Standby, CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.TargetPosition = this.WindowCovering.CurrentPosition; + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.STOPPED; this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Stopped`); } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.CurrentPosition},` + - ` TargetPosition: ${this.TargetPosition}, PositionState: ${this.PositionState},`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.WindowCovering.CurrentPosition},` + + ` TargetPosition: ${this.WindowCovering.TargetPosition}, PositionState: ${this.WindowCovering.PositionState},`); if (!this.device.blindTilt?.hide_lightsensor) { - this.set_minLux = this.minLux(); - this.set_maxLux = this.maxLux(); - this.spaceBetweenLevels = 9; + const set_minLux = this.device.blindTilt?.set_minLux ?? 1; + const set_maxLux = this.device.blindTilt?.set_maxLux ?? 6001; + const spaceBetweenLevels = 9; + + if (this.LightSensor?.CurrentAmbientLightLevel === 0) { + this.LightSensor!.CurrentAmbientLightLevel = 0.0001; + } // Brightness - switch (this.BLE_CurrentAmbientLightLevel) { + switch (serviceData.lightLevel) { case 1: - this.CurrentAmbientLightLevel = this.set_minLux; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = set_minLux; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); break; case 2: - this.CurrentAmbientLightLevel = (this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels; - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel},` + - ` Calculation: ${(this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels}`, - ); + this.LightSensor!.CurrentAmbientLightLevel = (set_maxLux - set_minLux) / spaceBetweenLevels; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel},` + + ` Calculation: ${(set_maxLux - set_minLux) / spaceBetweenLevels}`); break; case 3: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 2; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 2; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); break; case 4: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 3; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 3; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); break; case 5: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 4; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 4; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); break; case 6: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 5; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 5; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); break; case 7: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 6; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 6; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); break; case 8: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 7; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 7; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); break; case 9: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 8; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 8; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); break; case 10: default: - this.CurrentAmbientLightLevel = this.set_maxLux; + this.LightSensor!.CurrentAmbientLightLevel = set_maxLux; this.debugLog(); } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel},` + - ` CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel},` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } // Battery - this.BatteryLevel = Number(this.BLE_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(serviceData.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` + ` StatusLowBattery: ${this.StatusLowBattery}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); } - async openAPIparseStatus(): Promise { + + /** + * Parse the device status from the SwitchBot OpenAPI + */ + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); - const [homekitPosition, homekitTiltAngle] = this.mapDeviceValuesToHomekitValues(Number(this.OpenAPI_CurrentPosition), - String(this.OpenAPI_Direction)); - this.debugLog(` device: ${this.OpenAPI_CurrentPosition} => HK: ${homekitPosition}`); + const [homekitPosition, homekitTiltAngle] = this.mapDeviceValuesToHomekitValues(Number(deviceStatus.body.slidePosition), + String(deviceStatus.body.direction)); + this.debugLog(` device: ${deviceStatus.body.slidePosition} => HK: ${homekitPosition}`); - this.CurrentPosition = homekitPosition; + this.WindowCovering!.CurrentPosition = homekitPosition; // CurrentPosition await this.setMinMax(); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.CurrentPosition}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.WindowCovering.CurrentPosition}`); if (homekitTiltAngle) { - this.CurrentHorizontalTiltAngle = homekitTiltAngle!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentHorizontalTiltAngle: ${this.CurrentHorizontalTiltAngle}`); + this.WindowCovering.CurrentHorizontalTiltAngle = homekitTiltAngle!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentHorizontalTiltAngle: ${this.WindowCovering.CurrentHorizontalTiltAngle}`); } if (this.setNewTarget) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Checking Status ...`); } - if (this.setNewTarget && this.OpenAPI_InMotion) { + if (this.setNewTarget && deviceStatus.body.moving) { await this.setMinMax(); - if (this.TargetPosition > this.CurrentPosition || (homekitTiltAngle && this.TargetHorizontalTiltAngle !== this.CurrentHorizontalTiltAngle)) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Closing, CurrentPosition: ${this.CurrentPosition} `); - this.PositionState = this.hap.Characteristic.PositionState.INCREASING; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} INCREASING PositionState: ${this.PositionState}`); - } else if (this.TargetPosition < this.CurrentPosition) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Opening, CurrentPosition: ${this.CurrentPosition} `); - this.PositionState = this.hap.Characteristic.PositionState.DECREASING; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} DECREASING PositionState: ${this.PositionState}`); + if (this.WindowCovering.TargetPosition > this.WindowCovering.CurrentPosition + || (homekitTiltAngle && this.WindowCovering.TargetHorizontalTiltAngle !== this.WindowCovering.CurrentHorizontalTiltAngle)) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Closing, CurrentPosition: ${this.WindowCovering.CurrentPosition} `); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.INCREASING; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} INCREASING` + + ` PositionState: ${this.WindowCovering.PositionState}`); + } else if (this.WindowCovering.TargetPosition < this.WindowCovering.CurrentPosition) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Opening, CurrentPosition: ${this.WindowCovering.CurrentPosition} `); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.DECREASING; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} DECREASING` + + ` PositionState: ${this.WindowCovering.PositionState}`); } else { - this.debugLog( - `${this.device.deviceType}: ${this.CurrentPosition} Standby because reached position,` + ` CurrentPosition: ${this.CurrentPosition}`, - ); - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} STOPPED PositionState: ${this.PositionState}`); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} Standby because reached position,` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.STOPPED; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} STOPPED` + + ` PositionState: ${this.WindowCovering.PositionState}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Standby because device not moving,` + ` CurrentPosition: ${this.CurrentPosition}`, - ); - this.TargetPosition = this.CurrentPosition; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Standby because device not moving,` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.TargetPosition = this.WindowCovering.CurrentPosition; if (homekitTiltAngle) { - this.TargetHorizontalTiltAngle = this.CurrentHorizontalTiltAngle; + this.WindowCovering.TargetHorizontalTiltAngle = this.WindowCovering.CurrentHorizontalTiltAngle; } - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.STOPPED; this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Stopped`); } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.CurrentPosition},` + - ` TargetPosition: ${this.TargetPosition}, PositionState: ${this.PositionState},`, - ); - + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.WindowCovering.CurrentPosition},` + + ` TargetPosition: ${this.WindowCovering.TargetPosition}, PositionState: ${this.WindowCovering.PositionState},`); if (!this.device.blindTilt?.hide_lightsensor) { - this.set_minLux = this.minLux(); - this.set_maxLux = this.maxLux(); + const set_minLux = this.device.blindTilt?.set_minLux ?? 1; + const set_maxLux = this.device.blindTilt?.set_maxLux ?? 6001; // Brightness - switch (this.OpenAPI_CurrentAmbientLightLevel) { + switch (deviceStatus.body.brightness) { case 'dim': - this.CurrentAmbientLightLevel = this.set_minLux; + this.LightSensor!.CurrentAmbientLightLevel = set_minLux; break; case 'bright': default: - this.CurrentAmbientLightLevel = this.set_maxLux; + this.LightSensor!.CurrentAmbientLightLevel = set_maxLux; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } // BatteryLevel - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - } - if (Number.isNaN(this.BatteryLevel)) { - this.BatteryLevel = 100; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + } + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; + } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); + + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + let deviceVersion: string; + if (version?.includes('.') === false) { + const replace = version?.replace(/^V|-.*$/g, ''); + const match = replace?.match(/.{1,1}/g); + const blindTiltVersion = match?.join('.') ?? '0.0.0'; + deviceVersion = blindTiltVersion; + } else { + deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` - + ` StatusLowBattery: ${this.StatusLowBattery}`); - - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); } async refreshStatus(): Promise { @@ -490,10 +455,8 @@ export class BlindTilt { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -510,21 +473,13 @@ export class BlindTilt { // Start to monitor advertisement packets (async () => { // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'x', - id: this.device.bleMac, - }); + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); - if (this.device.bleMac === ad.address && ad.model === 'x') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_Calibration = ad.serviceData.calibration; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.BLE_InMotion = ad.serviceData.inMotion; - this.BLE_CurrentPosition = ad.serviceData.position; - this.BLE_CurrentAmbientLightLevel = ad.serviceData.lightLevel; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } @@ -534,83 +489,27 @@ export class BlindTilt { // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: 'c', - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.BLE_Calibration = ad.serviceData.calibration; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.BLE_InMotion = ad.serviceData.inMotion; - this.BLE_CurrentPosition = ad.serviceData.position; - this.BLE_CurrentAmbientLightLevel = ad.serviceData.lightLevel; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} calibration: ${ad.serviceData.calibration}, ` + - `position: ${ad.serviceData.position}, lightLevel: ${ad.serviceData.lightLevel}, battery: ${ad.serviceData.battery}, ` + - `inMotion: ${ad.serviceData.inMotion}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_CurrentPosition = deviceStatus.body.slidePosition; - this.OpenAPI_Direction = deviceStatus.body.direction; - this.OpenAPI_InMotion = deviceStatus.body.moving; - this.OpenAPI_CurrentAmbientLightLevel = deviceStatus.body.brightness; - this.OpenAPI_BatteryLevel = deviceStatus.body.battery; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -618,10 +517,35 @@ export class BlindTilt { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { slidePosition, battery, lightLevel } = context; + const { CurrentPosition } = this.WindowCovering; + const { BatteryLevel } = this.Battery; + const { CurrentAmbientLightLevel } = this.LightSensor ?? {}; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (slidePosition, battery, lightLevel) = Webhook:(${slidePosition}, ${battery},` + + ` ${lightLevel}), current:(${CurrentPosition}, ${BatteryLevel}, ${CurrentAmbientLightLevel})`); + this.WindowCovering.CurrentPosition = slidePosition; + this.Battery.BatteryLevel = battery; + if (!device.blindTilt?.hide_lightsensor) { + this.LightSensor!.CurrentAmbientLightLevel = lightLevel; + } + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -634,9 +558,8 @@ export class BlindTilt { await this.openAPIpushChanges(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, pushChanges will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, pushChanges will not happen.`); } // Refresh the status from the API interval(15000) @@ -649,7 +572,7 @@ export class BlindTilt { async BLEpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`); - if (this.TargetPosition !== this.CurrentPosition) { + if (this.WindowCovering.TargetPosition !== this.WindowCovering.CurrentPosition) { const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -657,33 +580,29 @@ export class BlindTilt { .join(':') .toLowerCase(); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE Address: ${this.device.bleMac}`); - this.SilentPerformance(); - const adjustedMode = this.setPositionMode || null; - if (adjustedMode === null) { - this.Mode = 'Default Mode'; - } - this.debugLog(`${this.accessory.displayName} Mode: ${this.Mode}`); + const { setPositionMode, Mode }: { setPositionMode: number; Mode: string; } = await this.setPerformance(); + this.debugLog(`${this.accessory.displayName} Mode: ${Mode}`); if (switchbot !== false) { switchbot .discover({ model: 'c', quick: true, id: this.device.bleMac }) .then(async (device_list: any) => { - this.infoLog(`${this.accessory.displayName} Target Position: ${this.TargetPosition}`); - return await this.retry({ - max: this.maxRetry(), + this.infoLog(`${this.accessory.displayName} Target Position: ${this.WindowCovering.TargetPosition}`); + return await this.retryBLE({ + max: await this.maxRetryBLE(), fn: async () => { - return await device_list[0].runToPos(100 - Number(this.TargetPosition), adjustedMode); + return await device_list[0].runToPos(100 - Number(this.WindowCovering.TargetPosition), setPositionMode); }, }); }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `Target Position: ${this.WindowCovering.TargetPosition} sent over BLE, sent successfully`); }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else { @@ -691,50 +610,35 @@ export class BlindTilt { await this.BLEPushConnection(); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges, CurrentPosition & TargetPosition Are the Same.` + - ` CurrentPosition: ${this.CurrentPosition}, TargetPosition ${this.TargetPosition}`, - ); - } - } - - async retry({ max, fn }: { max: number; fn: { (): any; (): Promise } }): Promise { - return fn().catch(async (e: any) => { - if (max === 0) { - throw e; - } - this.infoLog(e); - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Retrying`); - await sleep(1000); - return this.retry({ max: max - 1, fn }); - }); - } - - maxRetry(): number { - if (this.device.maxRetry) { - return this.device.maxRetry; - } else { - return 5; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges, CurrentPosition & TargetPosition Are the Same.` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}, TargetPosition ${this.WindowCovering.TargetPosition}`); } } async openAPIpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`); const hasDifferentAndRelevantHorizontalTiltAngle = - this.mappingMode === BlindTiltMappingMode.UseTiltForDirection && this.TargetHorizontalTiltAngle !== this.CurrentHorizontalTiltAngle; - if (this.TargetPosition !== this.CurrentPosition || hasDifferentAndRelevantHorizontalTiltAngle || this.device.disableCaching) { - const [direction, position] = this.mapHomekitValuesToDeviceValues(Number(this.TargetPosition), Number(this.TargetHorizontalTiltAngle)); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Pushing ${this.TargetPosition} (device = ${direction};${position})`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Mode: ${this.Mode}`); + this.mappingMode === BlindTiltMappingMode.UseTiltForDirection + && this.WindowCovering.TargetHorizontalTiltAngle !== this.WindowCovering.CurrentHorizontalTiltAngle; + if (this.WindowCovering.TargetPosition !== this.WindowCovering.CurrentPosition + || hasDifferentAndRelevantHorizontalTiltAngle || this.device.disableCaching) { + const [direction, position] = this.mapHomekitValuesToDeviceValues(Number(this.WindowCovering.TargetPosition), + Number(this.WindowCovering.TargetHorizontalTiltAngle)); + const { Mode }: { setPositionMode: number; Mode: string; } = await this.setPerformance(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` Pushing ${this.WindowCovering.TargetPosition} (device = ${direction};${position})`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Mode: ${Mode}`); let bodyChange = ''; if (position === 100) { bodyChange = JSON.stringify({ command: 'fullyOpen', + parameter: 'default', commandType: 'command', }); } else if (position === 0) { bodyChange = JSON.stringify({ command: direction === 'up' ? 'closeUp' : 'closeDown', + parameter: 'default', commandType: 'command', }); } else { @@ -757,25 +661,24 @@ export class BlindTilt { this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus body: ${JSON.stringify(deviceStatus.body)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No OpenAPI Changes, CurrentPosition & TargetPosition Are the Same.` + - ` CurrentPosition: ${this.CurrentPosition}, TargetPosition ${this.TargetPosition}` + - ` CurrentHorizontalTiltAngle: ${this.CurrentHorizontalTiltAngle}, TargetPosition ${this.TargetHorizontalTiltAngle}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No OpenAPI Changes, CurrentPosition & TargetPosition Are the Same.` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}, TargetPosition ${this.WindowCovering.TargetPosition}` + + ` CurrentHorizontalTiltAngle: ${this.WindowCovering.CurrentHorizontalTiltAngle},` + + ` TargetPosition ${this.WindowCovering.TargetHorizontalTiltAngle}`); } } @@ -783,16 +686,16 @@ export class BlindTilt { * Handle requests to set the value of the "Target Horizontal Tilt" characteristic */ async TargetHorizontalTiltAngleSet(value: CharacteristicValue): Promise { - if (this.TargetHorizontalTiltAngle === this.accessory.context.TargetHorizontalTiltAngle) { + if (this.WindowCovering.TargetHorizontalTiltAngle === this.accessory.context.TargetHorizontalTiltAngle) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set TargetHorizontalTiltAngle: ${value}`); } else { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set TargetHorizontalTiltAngle: ${value}`); } //value = value < 0 ? -90 : 90; - this.TargetHorizontalTiltAngle = value; + this.WindowCovering.TargetHorizontalTiltAngle = value; if (this.device.mqttURL) { - this.mqttPublish('TargetHorizontalTiltAngle', this.TargetHorizontalTiltAngle); + this.mqttPublish('TargetHorizontalTiltAngle', this.WindowCovering.TargetHorizontalTiltAngle.toString()); } this.startUpdatingBlindTiltIfNeeded(); @@ -802,15 +705,15 @@ export class BlindTilt { * Handle requests to set the value of the "Target Position" characteristic */ async TargetPositionSet(value: CharacteristicValue): Promise { - if (this.TargetPosition === this.accessory.context.TargetPosition) { + if (this.WindowCovering.TargetPosition === this.accessory.context.TargetPosition) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set TargetPosition: ${value}`); } else { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set TargetPosition: ${value}`); } - this.TargetPosition = value; + this.WindowCovering.TargetPosition = value; if (this.device.mqttURL) { - this.mqttPublish('TargetPosition', this.TargetPosition); + this.mqttPublish('TargetPosition', this.WindowCovering.TargetPosition.toString()); } this.startUpdatingBlindTiltIfNeeded(); } @@ -818,39 +721,37 @@ export class BlindTilt { async startUpdatingBlindTiltIfNeeded(): Promise { await this.setMinMax(); this.debugLog('setMinMax'); - if (this.TargetPosition > this.CurrentPosition || this.TargetHorizontalTiltAngle !== this.CurrentHorizontalTiltAngle) { - this.PositionState = this.hap.Characteristic.PositionState.INCREASING; + if (this.WindowCovering.TargetPosition > this.WindowCovering.CurrentPosition + || this.WindowCovering.TargetHorizontalTiltAngle !== this.WindowCovering.CurrentHorizontalTiltAngle) { + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.INCREASING; this.setNewTarget = true; - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} value: ${this.CurrentPosition},` + ` CurrentPosition: ${this.CurrentPosition}`, - ); - } else if (this.TargetPosition < this.CurrentPosition) { - this.PositionState = this.hap.Characteristic.PositionState.DECREASING; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} value: ${this.WindowCovering.CurrentPosition},` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + } else if (this.WindowCovering.TargetPosition < this.WindowCovering.CurrentPosition) { + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.DECREASING; this.setNewTarget = true; - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} value: ${this.CurrentPosition},` + ` CurrentPosition: ${this.CurrentPosition}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} value: ${this.WindowCovering.CurrentPosition},` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); } else { - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.STOPPED; this.setNewTarget = false; - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} value: ${this.CurrentPosition},` + ` CurrentPosition: ${this.CurrentPosition}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} value: ${this.WindowCovering.CurrentPosition},` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); } - this.windowCoveringService.setCharacteristic(this.hap.Characteristic.PositionState, this.PositionState); - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); + this.WindowCovering.Service.setCharacteristic(this.hap.Characteristic.PositionState, this.WindowCovering.PositionState); + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); /** * If Blind Tilt movement time is short, the moving flag from backend is always false. * The minimum time depends on the network control latency. */ clearTimeout(this.setNewTargetTimer); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateRate: ${this.updateRate}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateRate: ${this.deviceUpdateRate}`); if (this.setNewTarget) { this.setNewTargetTimer = setTimeout(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} setNewTarget ${this.setNewTarget} timeout`); this.setNewTarget = false; - }, this.updateRate * 1000); + }, this.deviceUpdateRate * 1000); } this.doBlindTiltUpdate.next(); } @@ -859,152 +760,105 @@ export class BlindTilt { await this.setMinMax(); // CurrentHorizontalTiltAngle if (this.mappingMode === BlindTiltMappingMode.UseTiltForDirection) { - if (this.CurrentHorizontalTiltAngle === undefined || Number.isNaN(this.CurrentHorizontalTiltAngle)) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentHorizontalTiltAngle: ${this.CurrentHorizontalTiltAngle}`); + if (this.WindowCovering.CurrentHorizontalTiltAngle === undefined || Number.isNaN(this.WindowCovering.CurrentHorizontalTiltAngle)) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentHorizontalTiltAngle: ${this.WindowCovering.CurrentHorizontalTiltAngle}`); } else { if (this.device.mqttURL) { - this.mqttPublish('CurrentHorizontalTiltAngle', this.CurrentHorizontalTiltAngle); + this.mqttPublish('CurrentHorizontalTiltAngle', this.WindowCovering.CurrentHorizontalTiltAngle.toString()); } - this.accessory.context.CurrentHorizontalTiltAngle = this.CurrentHorizontalTiltAngle; - this.windowCoveringService.updateCharacteristic( + this.accessory.context.CurrentHorizontalTiltAngle = this.WindowCovering.CurrentHorizontalTiltAngle; + this.WindowCovering.Service.updateCharacteristic( this.hap.Characteristic.CurrentHorizontalTiltAngle, - Number(this.CurrentHorizontalTiltAngle), - ); + Number(this.WindowCovering.CurrentHorizontalTiltAngle)); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} - updateCharacteristic CurrentHorizontalTiltAngle: ${this.CurrentHorizontalTiltAngle}`); + updateCharacteristic CurrentHorizontalTiltAngle: ${this.WindowCovering.CurrentHorizontalTiltAngle}`); } } // CurrentPosition - if (this.CurrentPosition === undefined || Number.isNaN(this.CurrentPosition)) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.CurrentPosition}`); + if (this.WindowCovering.CurrentPosition === undefined || Number.isNaN(this.WindowCovering.CurrentPosition)) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.WindowCovering.CurrentPosition}`); } else { if (this.device.mqttURL) { - this.mqttPublish('CurrentPosition', this.CurrentPosition); + this.mqttPublish('CurrentPosition', this.WindowCovering.CurrentPosition.toString()); } - this.accessory.context.CurrentPosition = this.CurrentPosition; - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.CurrentPosition, Number(this.CurrentPosition)); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic CurrentPosition: ${this.CurrentPosition}`); + this.accessory.context.CurrentPosition = this.WindowCovering.CurrentPosition; + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, Number(this.WindowCovering.CurrentPosition)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` updateCharacteristic CurrentPosition: ${this.WindowCovering.CurrentPosition}`); } // PositionState - if (this.PositionState === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} PositionState: ${this.PositionState}`); + if (this.WindowCovering.PositionState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} PositionState: ${this.WindowCovering.PositionState}`); } else { if (this.device.mqttURL) { - this.mqttPublish('PositionState', this.PositionState); + this.mqttPublish('PositionState', this.WindowCovering.PositionState.toString()); } - this.accessory.context.PositionState = this.PositionState; - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.PositionState, Number(this.PositionState)); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic PositionState: ${this.PositionState}`); + this.accessory.context.PositionState = this.WindowCovering.PositionState; + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.PositionState, Number(this.WindowCovering.PositionState)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` PositionState: ${this.WindowCovering.PositionState}`); } // TargetPosition - if (this.TargetPosition === undefined || Number.isNaN(this.TargetPosition)) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} TargetPosition: ${this.TargetPosition}`); + if (this.WindowCovering.TargetPosition === undefined || Number.isNaN(this.WindowCovering.TargetPosition)) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} TargetPosition: ${this.WindowCovering.TargetPosition}`); } else { if (this.device.mqttURL) { - this.mqttPublish('TargetPosition', this.TargetPosition); + this.mqttPublish('TargetPosition', this.WindowCovering.TargetPosition.toString()); } - this.accessory.context.TargetPosition = this.TargetPosition; - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.TargetPosition, Number(this.TargetPosition)); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: ${this.TargetPosition}`); + this.accessory.context.TargetPosition = this.WindowCovering.TargetPosition; + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, Number(this.WindowCovering.TargetPosition)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetPosition: ${this.WindowCovering.TargetPosition}`); } // CurrentAmbientLightLevel if (!this.device.blindTilt?.hide_lightsensor) { - if (this.CurrentAmbientLightLevel === undefined || Number.isNaN(this.CurrentAmbientLightLevel)) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + if (this.LightSensor!.CurrentAmbientLightLevel === undefined || Number.isNaN(this.LightSensor!.CurrentAmbientLightLevel)) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } else { if (this.device.mqttURL) { - this.mqttPublish('CurrentAmbientLightLevel', this.CurrentAmbientLightLevel); + this.mqttPublish('CurrentAmbientLightLevel', this.LightSensor!.CurrentAmbientLightLevel.toString()); } - this.accessory.context.CurrentAmbientLightLevel = this.CurrentAmbientLightLevel; - this.lightSensorService?.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.CurrentAmbientLightLevel); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` updateCharacteristic CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`, - ); + this.accessory.context.CurrentAmbientLightLevel = this.LightSensor!.CurrentAmbientLightLevel; + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.LightSensor!.CurrentAmbientLightLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` updateCharacteristic CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } } // BatteryLevel - if (this.BatteryLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}`); + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); } else { if (this.device.mqttURL) { - this.mqttPublish('BatteryLevel', this.BatteryLevel); + this.mqttPublish('BatteryLevel', this.Battery.BatteryLevel.toString()); } - this.accessory.context.BatteryLevel = this.BatteryLevel; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.BatteryLevel); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery?.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); } // StatusLowBattery - if (this.StatusLowBattery === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); } else { if (this.device.mqttURL) { - this.mqttPublish('StatusLowBattery', this.StatusLowBattery); - } - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); - } - } - - /* - * Publish MQTT message for topics of - * 'homebridge-switchbot/blindtilt/xx:xx:xx:xx:xx:xx' - */ - mqttPublish(topic: string, message: any) { - const mac = this.device.deviceId - ?.toLowerCase() - .match(/[\s\S]{1,2}/g) - ?.join(':'); - const options = this.device.mqttPubOptions || {}; - this.mqttClient?.publish(`homebridge-switchbot/blindtilt/${mac}/${topic}`, `${message}`, options); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT message: ${topic}/${message} options:${JSON.stringify(options)}`); - } - - /* - * Setup MQTT hadler if URL is specified. - */ - async setupMqtt(device: device & devicesConfig): Promise { - if (device.mqttURL) { - try { - const { connectAsync } = asyncmqtt; - this.mqttClient = await connectAsync(device.mqttURL, device.mqttOptions || {}); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT connection has been established successfully.`); - this.mqttClient.on('error', (e: Error) => { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to publish MQTT messages. ${e}`); - }); - } catch (e) { - this.mqttClient = null; - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to establish MQTT connection. ${e}`); + this.mqttPublish('StatusLowBattery', this.Battery.StatusLowBattery.toString()); } + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery?.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); } - } - - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); + if (this.Battery.ChargingState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ChargingState: ${this.Battery.ChargingState}`); } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'c', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); + if (this.device.mqttURL) { + this.mqttPublish('ChargingState', this.Battery.ChargingState.toString()); + } + this.accessory.context.ChargingState = this.Battery.ChargingState; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.ChargingState, this.Battery.ChargingState); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` ChargingState: ${this.Battery.ChargingState}`); } } @@ -1024,218 +878,74 @@ export class BlindTilt { } } - async SilentPerformance() { - if (Number(this.TargetPosition) > 50) { + async setPerformance() { + let setPositionMode: number; + let Mode: string; + if (Number(this.WindowCovering.TargetPosition) > 50) { if (this.device.blindTilt?.setOpenMode === '1') { - this.setPositionMode = 1; - this.Mode = 'Silent Mode'; + setPositionMode = 1; + Mode = 'Silent Mode'; + } else if (this.device.blindTilt?.setOpenMode === '0') { + setPositionMode = 0; + Mode = 'Performance Mode'; } else { - this.setPositionMode = 0; - this.Mode = 'Performance Mode'; + setPositionMode = 0; + Mode = 'Default Mode'; } } else { if (this.device.blindTilt?.setCloseMode === '1') { - this.setPositionMode = 1; - this.Mode = 'Silent Mode'; + setPositionMode = 1; + Mode = 'Silent Mode'; + } else if (this.device.blindTilt?.setOpenMode === '0') { + setPositionMode = 0; + Mode = 'Performance Mode'; } else { - this.setPositionMode = 0; - this.Mode = 'Performance Mode'; + setPositionMode = 0; + Mode = 'Default Mode'; } } + return { setPositionMode, Mode }; } async setMinMax(): Promise { if (this.device.blindTilt?.set_min) { - if (Number(this.CurrentPosition) <= this.device.blindTilt?.set_min) { - this.CurrentPosition = 0; + if (Number(this.WindowCovering.CurrentPosition) <= this.device.blindTilt?.set_min) { + this.WindowCovering.CurrentPosition = 0; } } if (this.device.blindTilt?.set_max) { - if (Number(this.CurrentPosition) >= this.device.blindTilt?.set_max) { - this.CurrentPosition = 100; + if (Number(this.WindowCovering.CurrentPosition) >= this.device.blindTilt?.set_max) { + this.WindowCovering.CurrentPosition = 100; } } if (this.mappingMode === BlindTiltMappingMode.UseTiltForDirection) { - this.CurrentHorizontalTiltAngle = Number(this.CurrentHorizontalTiltAngle) < 0 ? -90 : 90; - } - } - - minStep(device: device & devicesConfig): number { - if (device.blindTilt?.set_minStep) { - this.set_minStep = device.blindTilt?.set_minStep; - } else { - this.set_minStep = 1; - } - return this.set_minStep; - } - - minLux(): number { - if (this.device.blindTilt?.set_minLux) { - this.set_minLux = this.device.blindTilt?.set_minLux; - } else { - this.set_minLux = 1; - } - return this.set_minLux; - } - - maxLux(): number { - if (this.device.blindTilt?.set_maxLux) { - this.set_maxLux = this.device.blindTilt?.set_maxLux; - } else { - this.set_maxLux = 6001; - } - return this.set_maxLux; - } - - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - if (this.updateRate > device.scanDuration) { - this.scanDuration = this.updateRate; - if (this.BLE) { - this.warnLog( - `${this.device.deviceType}: ` + - `${this.accessory.displayName} scanDuration is less than updateRate, overriding scanDuration with updateRate`, - ); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - } - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - if (this.updateRate > 1) { - this.scanDuration = this.updateRate; - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - } - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); + this.WindowCovering.CurrentHorizontalTiltAngle = Number(this.WindowCovering.CurrentHorizontalTiltAngle) < 0 ? -90 : 90; } } async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentHorizontalTiltAngle, 90); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetHorizontalTiltAngle, 90); } } async apiError(e: any): Promise { - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.PositionState, e); - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); - if (!this.device.curtain?.hide_lightsensor) { - this.lightSensorService?.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, e); - } - if (this.BLE) { - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); - } - //throw new this.platform.api.hap.HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - } - - async deviceContext() { - if (this.CurrentPosition === undefined) { - this.CurrentPosition = 0; - } else { - this.CurrentPosition = this.accessory.context.CurrentPosition; - } - - if (this.TargetPosition === undefined) { - this.TargetPosition = 0; - } else { - this.TargetPosition = this.accessory.context.TargetPosition; - } - - if (this.PositionState === undefined) { - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; - } else { - this.PositionState = this.accessory.context.PositionState; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.PositionState, e); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentHorizontalTiltAngle, e); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetHorizontalTiltAngle, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.ChargingState, e); + if (!this.device.blindTilt?.hide_lightsensor) { + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, e); + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.StatusActive, e); } } @@ -1358,130 +1068,4 @@ export class BlindTilt { } } } - - async refreshRate(device: device & devicesConfig): Promise { - // refreshRate - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - // updateRate - if (device?.blindTilt?.updateRate) { - this.updateRate = device?.blindTilt?.updateRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Blind Tilt updateRate: ${this.updateRate}`); - } else { - this.updateRate = 2; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default Blind Tilt updateRate: ${this.updateRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.blindTilt) { - config = device.blindTilt; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.mqttURL !== undefined) { - config['mqttURL'] = device.mqttURL; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.maxRetry !== undefined) { - config['maxRetry'] = device.maxRetry; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (device.blindTilt?.mode === undefined) { - config['mode'] = BlindTiltMappingMode.OnlyUp; - } else { - config['mode'] = device.blindTilt?.mode; - } - - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; - } } diff --git a/src/device/bot.ts b/src/device/bot.ts index 7b581601..f710d4b6 100644 --- a/src/device/bot.ts +++ b/src/device/bot.ts @@ -1,114 +1,155 @@ +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * bot.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { sleep } from '../utils.js'; +import { deviceBase } from './device.js'; import { interval, Subject } from 'rxjs'; -import { SwitchBotPlatform } from '../platform.js'; +import { Devices } from '../settings.js'; import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { Service, PlatformAccessory, CharacteristicValue, API, Logging, HAP } from 'homebridge'; -import { device, devicesConfig, deviceStatus, serviceData, Devices, SwitchBotPlatformConfig } from '../settings.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class Bot { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class Bot extends deviceBase { // Services - fanService?: Service; - doorService?: Service; - lockService?: Service; - faucetService?: Service; - windowService?: Service; - switchService?: Service; - outletService?: Service; - batteryService: Service; - garageDoorService?: Service; - windowCoveringService?: Service; - statefulProgrammableSwitchService?: Service; - - // Characteristic Values - On!: CharacteristicValue; - BatteryLevel!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - StatusLowBattery!: CharacteristicValue; - - // OpenAPI Status - OpenAPI_On: deviceStatus['power']; - OpenAPI_BatteryLevel: deviceStatus['battery']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - - // BLE Status - BLE_On!: serviceData['state']; - BLE_Mode!: serviceData['mode']; - BLE_BatteryLevel!: serviceData['battery']; - - //BLE Others - BLE_IsConnected?: boolean; + private Battery: { + Name: CharacteristicValue; + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + }; + + private Switch?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private GarageDoor?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private Door?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private Window?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private WindowCovering?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private LockMechanism?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private Faucet?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private Fan?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private StatefulProgrammableSwitch?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private Outlet?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; // Config botMode!: string; allowPush?: boolean; doublePress!: number; - scanDuration!: number; botDeviceType!: string; pushRatePress!: number; - deviceLogging!: string; multiPressCount!: number; - deviceRefreshRate!: number; // Updates botUpdateInProgress!: boolean; doBotUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; + super(platform, accessory, device); // default placeholders - this.deviceLogs(device); - this.deviceType(device); - this.scan(device); - this.refreshRate(device); - this.PressOrSwitch(device); - this.allowPushChanges(device); - this.deviceContext(); - this.DoublePress(device); - this.deviceConfig(device); - - this.multiPressCount = 0; + this.getBotConfigSettings(device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doBotUpdate = new Subject(); this.botUpdateInProgress = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, 'SWITCHBOT-BOT-S1') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); + accessory.context.Battery = accessory.context.Battery ?? {}; + // Initialize Battery property + this.Battery = { + Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Characteristics + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .setCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery) + .setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE); // deviceType if (this.botDeviceType === 'switch') { + // Initialize Switch Service + accessory.context.Switch = accessory.context.Switch ?? {}; + this.Switch = { + Name: accessory.context.Switch.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Switch) ?? accessory.addService(this.hap.Service.Switch) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Switch = this.Switch as object; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Switch`); + + // Initialize Switch Characteristics + this.Switch.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Switch.Name) + .setCharacteristic(this.hap.Characteristic.On, this.Switch.On) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.Switch!.On; + }) + .onSet(this.OnSet.bind(this)); + + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -118,19 +159,33 @@ export class Bot { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.botDeviceType === 'garagedoor') { + // Initialize GarageDoor Service + accessory.context.GarageDoor = accessory.context.GarageDoor ?? {}; + this.GarageDoor = { + Name: accessory.context.GarageDoor.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.GarageDoorOpener) ?? accessory.addService(this.hap.Service.GarageDoorOpener) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.GarageDoor = this.GarageDoor as object; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Garage Door Opener`); - // Add switchService - const switchService = `${accessory.displayName} Switch`; - (this.switchService = accessory.getService(this.hap.Service.Switch) - || accessory.addService(this.hap.Service.Switch)), switchService; - this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Switch`); + // Initialize GarageDoor Characteristics + this.GarageDoor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.GarageDoor.Name) + .setCharacteristic(this.hap.Characteristic.ObstructionDetected, false) + .getCharacteristic(this.hap.Characteristic.TargetDoorState).setProps({ + validValues: [0, 100], + minValue: 0, + maxValue: 100, + minStep: 100, + }) + .onGet(() => { + return this.GarageDoor!.On; + }) + .onSet(this.OnSet.bind(this)); - this.switchService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.switchService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.switchService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.switchService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); - } else if (this.botDeviceType === 'garagedoor') { + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -140,20 +195,33 @@ export class Bot { this.removeWindowService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.botDeviceType === 'door') { + // Initialize Door Service + accessory.context.Door = accessory.context.Door ?? {}; + this.Door = { + Name: accessory.context.Door.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Door) ?? accessory.addService(this.hap.Service.Door) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Door = this.Door as object; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Door`); - // Add garageDoorService - const garageDoorService = `${accessory.displayName} Garage Door Opener`; - (this.garageDoorService = accessory.getService(this.hap.Service.GarageDoorOpener) - || accessory.addService(this.hap.Service.GarageDoorOpener)), garageDoorService; - this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Garage Door Opener`); + // Initialize Door Characteristics + this.Door.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Door.Name) + .setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED) + .getCharacteristic(this.hap.Characteristic.TargetPosition).setProps({ + validValues: [0, 100], + minValue: 0, + maxValue: 100, + minStep: 100, + }) + .onGet(() => { + return this.Door!.On; + }) + .onSet(this.OnSet.bind(this)); - this.garageDoorService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.garageDoorService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.garageDoorService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.garageDoorService.getCharacteristic(this.hap.Characteristic.TargetDoorState).onSet(this.OnSet.bind(this)); - this.garageDoorService.setCharacteristic(this.hap.Characteristic.ObstructionDetected, false); - } else if (this.botDeviceType === 'door') { + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeOutletService(accessory); @@ -163,28 +231,33 @@ export class Bot { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.botDeviceType === 'window') { + // Initialize Window Service + accessory.context.Window = accessory.context.Window ?? {}; + this.Window = { + Name: accessory.context.Window.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Window) ?? accessory.addService(this.hap.Service.Window) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Window = this.Window as object; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Window`); - // Add doorService - const doorService = `${accessory.displayName} Door`; - (this.doorService = accessory.getService(this.hap.Service.Door) - || accessory.addService(this.hap.Service.Door)), doorService; - this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Door`); - - this.doorService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.doorService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.doorService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.doorService - .getCharacteristic(this.hap.Characteristic.TargetPosition) - .setProps({ + // Initialize Window Characteristics + this.Window.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Window.Name) + .setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED) + .getCharacteristic(this.hap.Characteristic.TargetPosition).setProps({ validValues: [0, 100], minValue: 0, maxValue: 100, minStep: 100, }) + .onGet(() => { + return this.Window!.On; + }) .onSet(this.OnSet.bind(this)); - this.doorService.setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - } else if (this.botDeviceType === 'window') { + + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -194,28 +267,33 @@ export class Bot { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.botDeviceType === 'windowcovering') { + // Initialize WindowCovering Service + accessory.context.WindowCovering = accessory.context.WindowCovering ?? {}; + this.WindowCovering = { + Name: accessory.context.WindowCovering.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.WindowCovering) ?? accessory.addService(this.hap.Service.WindowCovering) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.WindowCovering = this.WindowCovering as object; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Window Covering`); - // Add windowService - const windowService = `${accessory.displayName} Window`; - (this.windowService = accessory.getService(this.hap.Service.Window) - || accessory.addService(this.hap.Service.Window)), windowService; - this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Window`); - - this.windowService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.windowService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.windowService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.windowService - .getCharacteristic(this.hap.Characteristic.TargetPosition) - .setProps({ + // Initialize WindowCovering Characteristics + this.WindowCovering.Service + .setCharacteristic(this.hap.Characteristic.Name, this.WindowCovering.Name) + .setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED) + .getCharacteristic(this.hap.Characteristic.TargetPosition).setProps({ validValues: [0, 100], minValue: 0, maxValue: 100, minStep: 100, }) + .onGet(() => { + return this.WindowCovering!.On; + }) .onSet(this.OnSet.bind(this)); - this.windowService.setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - } else if (this.botDeviceType === 'windowcovering') { + + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -225,28 +303,27 @@ export class Bot { this.removeWindowService(accessory); this.removeGarageDoorService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.botDeviceType === 'lock') { + // Initialize Lock Service + accessory.context.LockMechanism = accessory.context.LockMechanism ?? {}; + this.LockMechanism = { + Name: accessory.context.LockMechanism.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.LockMechanism) ?? accessory.addService(this.hap.Service.LockMechanism) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.LockMechanism = this.LockMechanism as object; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Lock`); - // Add windowCoveringService - const windowCoveringService = `${accessory.displayName} Window Covering`; - (this.windowCoveringService = accessory.getService(this.hap.Service.WindowCovering) - || accessory.addService(this.hap.Service.WindowCovering)), windowCoveringService; - this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Window Covering`); - - this.windowCoveringService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.windowCoveringService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.windowCoveringService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.windowCoveringService - .getCharacteristic(this.hap.Characteristic.TargetPosition) - .setProps({ - validValues: [0, 100], - minValue: 0, - maxValue: 100, - minStep: 100, + // Initialize Lock Characteristics + this.LockMechanism.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LockMechanism.Name) + .getCharacteristic(this.hap.Characteristic.LockTargetState) + .onGet(() => { + return this.LockMechanism!.On; }) .onSet(this.OnSet.bind(this)); - this.windowCoveringService.setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - } else if (this.botDeviceType === 'lock') { + + // Remove other services this.removeFanService(accessory); this.removeDoorService(accessory); this.removeOutletService(accessory); @@ -256,19 +333,27 @@ export class Bot { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.botDeviceType === 'faucet') { + // Initialize Faucet Service + accessory.context.Faucet = accessory.context.Faucet ?? {}; + this.Faucet = { + Name: accessory.context.Faucet.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Faucet) ?? accessory.addService(this.hap.Service.Faucet) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Faucet = this.Faucet as object; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Faucet`); - // Add lockService - const lockService = `${accessory.displayName} Lock`; - (this.lockService = accessory.getService(this.hap.Service.LockMechanism) - || accessory.addService(this.hap.Service.LockMechanism)), lockService; - this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Lock`); + // Initialize Faucet Characteristics + this.Faucet.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Faucet.Name) + .getCharacteristic(this.hap.Characteristic.Active) + .onGet(() => { + return this.Faucet!.On; + }) + .onSet(this.OnSet.bind(this)); - this.lockService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.lockService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.lockService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.lockService.getCharacteristic(this.hap.Characteristic.LockTargetState).onSet(this.OnSet.bind(this)); - } else if (this.botDeviceType === 'faucet') { + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -278,19 +363,27 @@ export class Bot { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.botDeviceType === 'fan') { + // Initialize Fan Service + accessory.context.Fan = accessory.context.Fan ?? {}; + this.Fan = { + Name: accessory.context.Fan.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Fanv2) ?? accessory.addService(this.hap.Service.Fanv2) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Fan = this.Fan as object; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Fan`); - // Add faucetService - const faucetService = `${accessory.displayName} Faucet`; - (this.faucetService = accessory.getService(this.hap.Service.Faucet) - || accessory.addService(this.hap.Service.Faucet)), faucetService; - this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Faucet`); + // Initialize Fan Characteristics + this.Fan.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Fan.Name) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.Fan!.On; + }) + .onSet(this.OnSet.bind(this)); - this.faucetService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.faucetService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.faucetService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.faucetService.getCharacteristic(this.hap.Characteristic.Active).onSet(this.OnSet.bind(this)); - } else if (this.botDeviceType === 'fan') { + // Remove other services this.removeLockService(accessory); this.removeDoorService(accessory); this.removeFaucetService(accessory); @@ -300,19 +393,28 @@ export class Bot { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.botDeviceType === 'stateful') { + // Initialize StatefulProgrammableSwitch Service + accessory.context.StatefulProgrammableSwitch = accessory.context.StatefulProgrammableSwitch ?? {}; + this.StatefulProgrammableSwitch = { + Name: accessory.context.StatefulProgrammableSwitch.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.StatefulProgrammableSwitch) + ?? accessory.addService(this.hap.Service.StatefulProgrammableSwitch) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.StatefulProgrammableSwitch = this.StatefulProgrammableSwitch as object; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Stateful Programmable Switch`); - // Add fanService - const fanService = `${accessory.displayName} Fan`; - (this.fanService = accessory.getService(this.hap.Service.Fan) - || accessory.addService(this.hap.Service.Fan)), fanService; - this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Fan`); + // Initialize StatefulProgrammableSwitch Characteristics + this.StatefulProgrammableSwitch.Service + .setCharacteristic(this.hap.Characteristic.Name, this.StatefulProgrammableSwitch.Name) + .getCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState) + .onGet(() => { + return this.StatefulProgrammableSwitch!.On; + }) + .onSet(this.OnSet.bind(this)); - this.fanService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.fanService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.fanService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.fanService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); - } else if (this.botDeviceType === 'stateful') { + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -322,21 +424,27 @@ export class Bot { this.removeWindowService(accessory); this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); + } else { + // Initialize Switch property + accessory.context.Outlet = accessory.context.Outlet ?? {}; + this.Outlet = { + Name: accessory.context.Outlet.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Outlet) ?? accessory.addService(this.hap.Service.Outlet) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Outlet = this.Outlet as object; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Outlet`); - // Add statefulProgrammableSwitchService - const statefulProgrammableSwitchService = `${accessory.displayName} Stateful Programmable Switch`; - (this.statefulProgrammableSwitchService = accessory.getService(this.hap.Service.StatefulProgrammableSwitch) || - accessory.addService(this.hap.Service.StatefulProgrammableSwitch)), statefulProgrammableSwitchService; - this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Stateful Programmable Switch`); - - this.statefulProgrammableSwitchService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.statefulProgrammableSwitchService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.statefulProgrammableSwitchService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.statefulProgrammableSwitchService - .getCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState) + // Initialize Outlet Characteristics + this.Outlet.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Outlet.Name) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.Outlet!.On; + }) .onSet(this.OnSet.bind(this)); - } else { + + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -346,30 +454,10 @@ export class Bot { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); - - // Add outletService - const outletService = `${accessory.displayName} Outlet`; - (this.outletService = accessory.getService(this.hap.Service.Outlet) - || accessory.addService(this.hap.Service.Outlet)), outletService; - this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Displaying as Outlet`); - - this.outletService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.outletService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.outletService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.outletService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); } - // batteryService - const batteryService = `${accessory.displayName} Battery`; - (this.batteryService = this.accessory.getService(this.hap.Service.Battery) - || accessory.addService(this.hap.Service.Battery)), batteryService; - - this.batteryService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Battery`); - if (!this.batteryService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.batteryService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Battery`); - } - this.batteryService.setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE); + // Retrieve initial values and updateHomekit + this.refreshStatus(); // Retrieve initial values and updateHomekit this.updateHomeKitCharacteristics(); @@ -381,28 +469,8 @@ export class Bot { await this.refreshStatus(); }); - //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - const { power, battery, deviceMode } = context; - const { On, BatteryLevel, botMode } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(power, battery, deviceMode) = ' + - `Webhook:(${power}, ${battery}, ${deviceMode}), ` + - `current:(${On}, ${BatteryLevel}, ${botMode})`); - this.On = power; - this.BatteryLevel = battery; - this.botMode = deviceMode; - this.updateHomeKitCharacteristics(); - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + // regisiter webhook event handler + this.registerWebhook(accessory, device); // Watch for Bot change events // We put in a debounce of 1000ms so we don't make duplicate calls @@ -411,7 +479,7 @@ export class Bot { tap(() => { this.botUpdateInProgress = true; }), - debounceTime(this.platform.config.options!.pushRate! * 1000), + debounceTime(this.devicePushRate * 1000), ) .subscribe(async () => { try { @@ -426,90 +494,90 @@ export class Bot { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + - ` Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed pushChanges with ${device.connectionType} Connection,` + + ` Error Message: ${JSON.stringify(e.message)}`); } this.botUpdateInProgress = false; }); } /** - * Parse the device status from the SwitchBot api + * Parse the device status from the SwitchBotBLE API */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // BLEmode (true if Switch Mode) | (false if Press Mode) - if (this.BLE_Mode) { - this.accessory.context.On = this.On; - if (this.On === undefined) { - this.On = Boolean(this.BLE_On); + if (serviceData.mode) { + this.accessory.context.On = await this.getOn(); + if (this.getOn() === undefined) { + this.setOn(Boolean(serviceData.state)); } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Switch Mode, mode: ${this.BLE_Mode}, On: ${this.On}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Switch Mode,` + + ` mode: ${serviceData.mode}, On: ${this.accessory.context.On}`); } else { - this.On = false; - this.accessory.context.On = this.On; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Press Mode, mode: ${this.BLE_Mode}, On: ${this.On}`); + this.setOn(false); + this.accessory.context.On = await this.getOn(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Press Mode,` + + ` mode: ${serviceData.mode}, On: ${this.accessory.context.On}`); } - this.BatteryLevel = Number(this.BLE_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(serviceData.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - if (Number.isNaN(this.BatteryLevel)) { - this.BatteryLevel = 100; + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` - + ` StatusLowBattery: ${this.StatusLowBattery}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); } - async openAPIparseStatus(): Promise { + + /** + * Parse the device status from the SwitchBot OpenAPI + */ + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); if (this.botMode === 'press') { - this.On = false; - this.accessory.context.On = this.On; + this.setOn(false); + this.accessory.context.On = await this.getOn(); } else { - this.accessory.context.On = this.On; - if (this.On === undefined) { - this.On = false; + this.accessory.context.On = await this.getOn(); + if (this.getOn() === undefined) { + this.setOn(false); } } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.accessory.context.On}`); // Battery - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - } - if (Number.isNaN(this.BatteryLevel)) { - this.BatteryLevel = 100; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + } + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; + } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); + + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` - + ` StatusLowBattery: ${this.StatusLowBattery}`); - - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; } /** @@ -524,10 +592,8 @@ export class Bot { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -544,99 +610,43 @@ export class Bot { // Start to monitor advertisement packets (async () => { // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'H', - id: this.device.bleMac, - }); + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); - if (this.device.bleMac === ad.address && ad.model === 'H') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_Mode = ad.serviceData.mode; - this.BLE_On = ad.serviceData.state; - this.BLE_BatteryLevel = ad.serviceData.battery; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: 'H', - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.BLE_Mode = ad.serviceData.mode; - this.BLE_On = ad.serviceData.state; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}, model: ${ad.serviceData.model}, modelName: ` + - `${ad.serviceData.modelName}, mode: ${ad.serviceData.mode}, state: ${ad.serviceData.state}, battery: ${ad.serviceData.battery}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_On = deviceStatus.body.power; - this.OpenAPI_BatteryLevel = deviceStatus.body.battery; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -644,10 +654,33 @@ export class Bot { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { power, battery, deviceMode } = context; + const { botMode } = this; + const On = await this.getOn(); + const { BatteryLevel } = this.Battery; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (power, battery, deviceMode) = Webhook:(${power}, ${battery}, ${deviceMode}),` + + ` current:(${On}, ${BatteryLevel}, ${botMode})`); + await this.setOn(power); + this.Battery.BatteryLevel = battery; + this.botMode = deviceMode; + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -667,9 +700,8 @@ export class Bot { await this.openAPIpushChanges(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, pushChanges will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, pushChanges will not happen.`); } // Refresh the status from the API interval(15000) @@ -682,8 +714,9 @@ export class Bot { async BLEpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`); - if (this.On !== this.accessory.context.On || this.allowPush) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges On: ${this.On} OnCached: ${this.accessory.context.On}`); + const On = await this.getOn(); + if (On !== this.accessory.context.On || this.allowPush) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges On: ${On} OnCached: ${this.accessory.context.On}`); const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -697,27 +730,27 @@ export class Bot { switchbot .discover({ model: 'H', quick: true, id: this.device.bleMac }) .then(async (device_list: { press: (arg0: { id: string | undefined }) => any }[]) => { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${On}`); return await device_list[0].press({ id: this.device.bleMac }); }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.accessory.context.On = this.On; + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `On: ${On} sent over BLE, sent successfully`); + this.accessory.context.On = On; setTimeout(() => { if (this.botDeviceType === 'switch') { - this.switchService?.getCharacteristic(this.hap.Characteristic.On).updateValue(this.On); + this.Switch?.Service.getCharacteristic(this.hap.Characteristic.On).updateValue(On); } else { - this.outletService?.getCharacteristic(this.hap.Characteristic.On).updateValue(this.On); + this.Outlet?.Service.getCharacteristic(this.hap.Characteristic.On).updateValue(On); } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}, Switch Timeout`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${On}, Switch Timeout`); }, 500); }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + - ` Connection & botMode: ${this.botMode}, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection & botMode: ${this.botMode}, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else if (this.botMode === 'switch') { @@ -725,11 +758,11 @@ export class Bot { switchbot .discover({ model: 'H', quick: true, id: this.device.bleMac }) .then(async (device_list: any) => { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); - return await this.retry({ - max: this.maxRetry(), + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${On}`); + return await this.retryBLE({ + max: await this.maxRetryBLE(), fn: async () => { - if (this.On) { + if (On) { return await device_list[0].turnOn({ id: this.device.bleMac }); } else { return await device_list[0].turnOff({ id: this.device.bleMac }); @@ -739,46 +772,43 @@ export class Bot { }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.accessory.context.On = this.On; + this.accessory.context.On = On; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + - ` Connection & botMode: ${this.botMode}, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection & botMode: ${this.botMode}, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else { this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bot Mode: ${this.botMode}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges.` + `On: ${this.On}, ` + `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges, On: ${On}, OnCached: ${this.accessory.context.On}`); } } async openAPIpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`); + let On = await this.getOn(); if (this.multiPressCount > 0) { this.debugLog(`${this.device.deviceType}: ${this.multiPressCount} request(s) queued.`); - this.On = true; + On = true; } - if (this.On !== this.accessory.context.On || this.allowPush || this.multiPressCount > 0) { + if (On !== this.accessory.context.On || this.allowPush || this.multiPressCount > 0) { let command = ''; - if (this.botMode === 'switch' && this.On) { + if (this.botMode === 'switch' && On) { command = 'turnOn'; - this.On = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Switch Mode, Turning ${this.On}`); - } else if (this.botMode === 'switch' && !this.On) { + On = true; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Switch Mode, Turning ${On}`); + } else if (this.botMode === 'switch' && !On) { command = 'turnOff'; - this.On = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Switch Mode, Turning ${this.On}`); + On = false; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Switch Mode, Turning ${On}`); } else if (this.botMode === 'press' || this.botMode === 'multipress') { command = 'press'; this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Press Mode`); - this.On = false; + On = false; } else { throw new Error(`${this.device.deviceType}: ${this.accessory.displayName} Device Parameters not set for this Bot.`); } @@ -800,8 +830,10 @@ export class Bot { this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus body: ${JSON.stringify(deviceStatus.body)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); @@ -810,23 +842,18 @@ export class Bot { this.multiPressCount--; if (this.multiPressCount > 0) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Multi Press Count: ${this.multiPressCount}`); - this.On = true; + On = true; this.openAPIpushChanges(); } } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.` + - `On: ${this.On}, ` + - `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges,` + + ` On: ${On}, OnCached: ${this.accessory.context.On}`); } } @@ -835,53 +862,105 @@ export class Bot { */ async OnSet(value: CharacteristicValue): Promise { if (this.botDeviceType === 'garagedoor') { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set TargetDoorState: ${value}`); - if (value === this.hap.Characteristic.TargetDoorState.CLOSED) { - this.On = false; - } else { - this.On = true; + if (this.GarageDoor) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set TargetDoorState: ${value}`); + if (value === this.hap.Characteristic.TargetDoorState.CLOSED) { + await this.setOn(false); + this.GarageDoor.On = false; + } else { + await this.setOn(true); + this.GarageDoor.On = true; + } } - } else if ( - this.botDeviceType === 'door' || - this.botDeviceType === 'window' || - this.botDeviceType === 'windowcovering' - ) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set TargetPosition: ${value}`); - if (value === 0) { - this.On = false; - } else { - this.On = true; + } else if (this.botDeviceType === 'door') { + if (this.Door) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set TargetPosition: ${value}`); + if (value === 0) { + await this.setOn(false); + this.Door.On = false; + } else { + await this.setOn(true); + this.Door.On = true; + } + } + } else if (this.botDeviceType === 'window') { + if (this.Window) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set TargetPosition: ${value}`); + if (value === 0) { + await this.setOn(false); + this.Window.On = false; + } else { + await this.setOn(true); + this.Window.On = true; + } + } + } else if (this.botDeviceType === 'windowcovering') { + if (this.WindowCovering) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set TargetPosition: ${value}`); + if (value === 0) { + await this.setOn(false); + this.WindowCovering.On = false; + } else { + await this.setOn(true); + this.WindowCovering.On = true; + } } } else if (this.botDeviceType === 'lock') { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set LockTargetState: ${value}`); - if (value === this.hap.Characteristic.LockTargetState.SECURED) { - this.On = false; - } else { - this.On = true; + if (this.LockMechanism) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set LockTargetState: ${value}`); + if (value === this.hap.Characteristic.LockTargetState.SECURED) { + await this.setOn(false); + this.LockMechanism.On = false; + } else { + await this.setOn(true); + this.LockMechanism.On = true; + } } } else if (this.botDeviceType === 'faucet') { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Active: ${value}`); - if (value === this.hap.Characteristic.Active.INACTIVE) { - this.On = false; - } else { - this.On = true; + if (this.Faucet) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Active: ${value}`); + if (value === this.hap.Characteristic.Active.INACTIVE) { + await this.setOn(false); + this.Faucet.On = false; + } else { + await this.setOn(true); + this.Faucet.On = true; + } } } else if (this.botDeviceType === 'stateful') { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set ProgrammableSwitchOutputState: ${value}`); - if (value === 0) { - this.On = false; - } else { - this.On = true; + if (this.StatefulProgrammableSwitch) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set ProgrammableSwitchOutputState: ${value}`); + if (value === 0) { + await this.setOn(false); + this.StatefulProgrammableSwitch.On = false; + } else { + await this.setOn(true); + this.StatefulProgrammableSwitch.On = true; + } } - } else { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set On: ${value}`); - if (this.device.bot?.mode === 'multipress') { - if (value === true) { - this.multiPressCount++; - this.debugLog(`${this.device.deviceType} set to Multi-Press. Multi-Press count: ${this.multiPressCount}`); + } else if (this.botDeviceType === 'switch') { + if (this.Switch) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set ProgrammableSwitchOutputState: ${value}`); + if (value === 0) { + await this.setOn(false); + this.Switch.On = false; + } else { + await this.setOn(true); + this.Switch.On = true; } } - this.On = value; + } else { + if (this.Outlet) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set On: ${value}`); + await this.setOn(Boolean(value)); + this.Outlet.On = value; + } + } + if (this.device.bot?.mode === 'multipress') { + if (value === true) { + this.multiPressCount++; + this.debugLog(`${this.device.deviceType} set to Multi-Press. Multi-Press count: ${this.multiPressCount}`); + } } this.doBotUpdate.next(); } @@ -892,536 +971,352 @@ export class Bot { async updateHomeKitCharacteristics(): Promise { // State if (this.botDeviceType === 'garagedoor') { - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.GarageDoor?.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.GarageDoor?.On}`); } else { - if (this.On) { - this.garageDoorService?.updateCharacteristic( - this.hap.Characteristic.TargetDoorState, - this.hap.Characteristic.TargetDoorState.OPEN, - ); - this.garageDoorService?.updateCharacteristic( - this.hap.Characteristic.CurrentDoorState, - this.hap.Characteristic.CurrentDoorState.OPEN, - ); - this.debugLog( - `${this.device.deviceType}: ` + `${this.accessory.displayName} updateCharacteristic TargetDoorState: Open, CurrentDoorState: Open`, - ); + if (this.GarageDoor.On) { + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.hap.Characteristic.TargetDoorState.OPEN); + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.OPEN); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetDoorState: Open, CurrentDoorState: Open (${this.GarageDoor.On})`); } else { - this.garageDoorService?.updateCharacteristic( - this.hap.Characteristic.TargetDoorState, - this.hap.Characteristic.TargetDoorState.CLOSED, - ); - this.garageDoorService?.updateCharacteristic( - this.hap.Characteristic.CurrentDoorState, - this.hap.Characteristic.CurrentDoorState.CLOSED, - ); - this.debugLog( - `${this.device.deviceType}: ` + `${this.accessory.displayName} updateCharacteristic TargetDoorState: Open, CurrentDoorState: Open`, - ); + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.hap.Characteristic.TargetDoorState.CLOSED); + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.CLOSED); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristicc` + + ` TargetDoorState: Open, CurrentDoorState: Open (${this.GarageDoor.On})`); } } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Garage Door On: ${this.On}`); + await this.setOn(Boolean(this.GarageDoor?.On)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Garage Door On: ${this.GarageDoor?.On}`); } else if (this.botDeviceType === 'door') { - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Door?.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Door?.On}`); } else { - if (this.On) { - this.doorService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); - this.doorService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); - this.doorService?.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 100, CurrentPosition: 100`); + if (this.Door.On) { + this.Door.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); + this.Door.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); + this.Door.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristicc` + + ` TargetPosition: 100, CurrentPosition: 100 (${this.Door.On})`); } else { - this.doorService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); - this.doorService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); - this.doorService?.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 0, CurrentPosition: 0`); + this.Door.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); + this.Door.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); + this.Door.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristicc` + + ` TargetPosition: 0, CurrentPosition: 0 (${this.Door.On})`); } } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Door On: ${this.On}`); + await this.setOn(Boolean(this.Door?.On)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Door On: ${this.Door?.On}`); } else if (this.botDeviceType === 'window') { - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Window?.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Window?.On}`); } else { - if (this.On) { - this.windowService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); - this.windowService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); - this.windowService?.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 100, CurrentPosition: 100`); + if (this.Window.On) { + this.Window.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); + this.Window.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); + this.Window.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristicc` + + ` TargetPosition: 100, CurrentPosition: 100 (${this.Window.On})`); } else { - this.windowService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); - this.windowService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); - this.windowService?.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 0, CurrentPosition: 0`); + this.Window.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); + this.Window.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); + this.Window.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristicc` + + ` TargetPosition: 0, CurrentPosition: 0 (${this.Window.On})`); } } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Window On: ${this.On}`); + await this.setOn(Boolean(this.Window?.On)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Window On: ${this.Window?.On}`); } else if (this.botDeviceType === 'windowcovering') { - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.WindowCovering?.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.WindowCovering?.On}`); } else { - if (this.On) { - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); - this.windowCoveringService?.updateCharacteristic( - this.hap.Characteristic.PositionState, - this.hap.Characteristic.PositionState.STOPPED, - ); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 100, CurrentPosition: 100`); + if (this.WindowCovering.On) { + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristicc` + + ` TargetPosition: 100, CurrentPosition: 100 (${this.WindowCovering.On})`); } else { - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); - this.windowCoveringService?.updateCharacteristic( - this.hap.Characteristic.PositionState, - this.hap.Characteristic.PositionState.STOPPED, - ); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 0, CurrentPosition: 0`); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristicc` + + ` TargetPosition: 0, CurrentPosition: 0 (${this.WindowCovering.On})`); } } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Window Covering On: ${this.On}`); + await this.setOn(Boolean(this.WindowCovering?.On)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Window Covering On: ${this.WindowCovering?.On}`); } else if (this.botDeviceType === 'lock') { - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.LockMechanism?.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LockMechanism?.On}`); } else { - if (this.On) { - this.lockService?.updateCharacteristic( - this.hap.Characteristic.LockTargetState, - this.hap.Characteristic.LockTargetState.UNSECURED, - ); - this.lockService?.updateCharacteristic( - this.hap.Characteristic.LockCurrentState, - this.hap.Characteristic.LockCurrentState.UNSECURED, - ); - this.debugLog( - `${this.device.deviceType}: ` + - `${this.accessory.displayName} updateCharacteristic LockTargetState: UNSECURED, LockCurrentState: UNSECURED`, - ); + if (this.LockMechanism.On) { + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockTargetState, + this.hap.Characteristic.LockTargetState.UNSECURED); + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, + this.hap.Characteristic.LockCurrentState.UNSECURED); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristicc` + + ` LockTargetState: UNSECURED, LockCurrentState: UNSECURED (${this.LockMechanism.On})`); } else { - this.lockService?.updateCharacteristic(this.hap.Characteristic.LockTargetState, this.hap.Characteristic.LockTargetState.SECURED); - this.lockService?.updateCharacteristic( - this.hap.Characteristic.LockCurrentState, - this.hap.Characteristic.LockCurrentState.SECURED, - ); - this.debugLog( - `${this.device.deviceType}: ` + `${this.accessory.displayName} updateCharacteristic LockTargetState: SECURED, LockCurrentState: SECURED`, - ); + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockTargetState, + this.hap.Characteristic.LockTargetState.SECURED); + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, + this.hap.Characteristic.LockCurrentState.SECURED); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` LockTargetState: SECURED, LockCurrentState: SECURED (${this.LockMechanism.On})`); } } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Lock On: ${this.On}`); + await this.setOn(Boolean(this.LockMechanism?.On)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Lock On: ${this.LockMechanism?.On}`); } else if (this.botDeviceType === 'faucet') { - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Faucet?.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Faucet?.On}`); } else { - if (this.On) { - this.faucetService?.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.ACTIVE); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.On}`); + if (this.Faucet.On) { + this.Faucet.Service.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.ACTIVE); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Faucet.On}`); } else { - this.faucetService?.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.INACTIVE); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.On}`); + this.Faucet.Service.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.INACTIVE); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Faucet.On}`); } } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Faucet On: ${this.On}`); + await this.setOn(Boolean(this.Faucet?.On)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Faucet On: ${this.Faucet?.On}`); } else if (this.botDeviceType === 'fan') { - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Fan?.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Fan?.On}`); } else { - if (this.On) { - this.fanService?.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + if (this.Fan.On) { + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.On, this.Fan.On); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.Fan.On}`); } else { - this.fanService?.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.On, this.Fan.On); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.Fan.On}`); } } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Fan On: ${this.On}`); + await this.setOn(Boolean(this.Fan?.On)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Fan On: ${this.Fan?.On}`); } else if (this.botDeviceType === 'stateful') { - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.StatefulProgrammableSwitch?.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.StatefulProgrammableSwitch?.On}`); } else { - if (this.On) { - this.statefulProgrammableSwitchService?.updateCharacteristic( - this.hap.Characteristic.ProgrammableSwitchEvent, - this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, - ); - this.statefulProgrammableSwitchService?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, 1); - this.debugLog( - `${this.device.deviceType}: ` + - `${this.accessory.displayName} updateCharacteristic ProgrammableSwitchEvent: SINGLE, ProgrammableSwitchOutputState: 1`, - ); + if (this.StatefulProgrammableSwitch.On) { + this.StatefulProgrammableSwitch.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, + this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS); + this.StatefulProgrammableSwitch.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, 1); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` ProgrammableSwitchEvent: SINGLE, ProgrammableSwitchOutputState: 1 (${this.StatefulProgrammableSwitch.On})`); } else { - this.statefulProgrammableSwitchService?.updateCharacteristic( - this.hap.Characteristic.ProgrammableSwitchEvent, - this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, - ); - this.statefulProgrammableSwitchService?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, 0); - this.debugLog( - `${this.device.deviceType}: ` + - `${this.accessory.displayName} updateCharacteristic ProgrammableSwitchEvent: SINGLE, ProgrammableSwitchOutputState: 0`, - ); + this.StatefulProgrammableSwitch.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, + this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS); + this.StatefulProgrammableSwitch.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, 0); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` ProgrammableSwitchEvent: SINGLE, ProgrammableSwitchOutputState: 0 (${this.StatefulProgrammableSwitch.On})`); } } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatefulProgrammableSwitch On: ${this.On}`); + await this.setOn(Boolean(this.StatefulProgrammableSwitch?.On)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatefulProgrammableSwitch On: ${this.StatefulProgrammableSwitch?.On}`); } else if (this.botDeviceType === 'switch') { - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Switch?.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Switch?.On}`); } else { - this.switchService?.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + this.Switch.Service.updateCharacteristic(this.hap.Characteristic.On, this.Switch.On); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.Switch.On}`); } + await this.setOn(Boolean(this.Switch?.On)); } else { - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Outlet?.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Outlet?.On}`); } else { - this.outletService?.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + this.Outlet.Service.updateCharacteristic(this.hap.Characteristic.On, this.Outlet.On); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.Outlet.On}`); } + await this.setOn(Boolean(this.Outlet?.On)); } - this.accessory.context.On = this.On; + this.accessory.context.On = await this.getOn(); // BatteryLevel - if (this.BatteryLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}`); + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); } else { - this.accessory.context.BatteryLevel = this.BatteryLevel; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.BatteryLevel); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); } // StatusLowBattery - if (this.StatusLowBattery === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); } else { - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); } } async removeOutletService(accessory: PlatformAccessory): Promise { // If outletService still present, then remove first - this.outletService = this.accessory.getService(this.hap.Service.Outlet); - if (this.outletService) { + if (this.Outlet?.Service) { + this.Outlet.Service = this.accessory.getService(this.hap.Service.Outlet) as Service; this.warnLog(`${this.device.deviceType}: ${accessory.displayName} Removing Leftover Outlet Service`); + accessory.removeService(this.Outlet.Service); } - accessory.removeService(this.outletService!); } async removeGarageDoorService(accessory: PlatformAccessory): Promise { // If garageDoorService still present, then remove first - this.garageDoorService = this.accessory.getService(this.hap.Service.GarageDoorOpener); - if (this.garageDoorService) { + if (this.GarageDoor?.Service) { + this.GarageDoor.Service = this.accessory.getService(this.hap.Service.GarageDoorOpener) as Service; this.warnLog(`${this.device.deviceType}: ${accessory.displayName} Removing Leftover Garage Door Service`); + accessory.removeService(this.GarageDoor.Service); } - accessory.removeService(this.garageDoorService!); } async removeDoorService(accessory: PlatformAccessory): Promise { // If doorService still present, then remove first - this.doorService = this.accessory.getService(this.hap.Service.Door); - if (this.doorService) { + if (this.Door?.Service) { + this.Door.Service = this.accessory.getService(this.hap.Service.Door) as Service; this.warnLog(`${this.device.deviceType}: ${accessory.displayName} Removing Leftover Door Service`); + accessory.removeService(this.Door.Service); } - accessory.removeService(this.doorService!); } async removeLockService(accessory: PlatformAccessory): Promise { // If lockService still present, then remove first - this.lockService = this.accessory.getService(this.hap.Service.LockMechanism); - if (this.lockService) { + if (this.LockMechanism?.Service) { + this.LockMechanism.Service = this.accessory.getService(this.hap.Service.LockMechanism) as Service; this.warnLog(`${this.device.deviceType}: ${accessory.displayName} Removing Leftover Lock Service`); + accessory.removeService(this.LockMechanism.Service); } - accessory.removeService(this.lockService!); } async removeFaucetService(accessory: PlatformAccessory): Promise { // If faucetService still present, then remove first - this.faucetService = this.accessory.getService(this.hap.Service.Faucet); - if (this.faucetService) { + if (this.Faucet?.Service) { + this.Faucet.Service = this.accessory.getService(this.hap.Service.Faucet) as Service; this.warnLog(`${this.device.deviceType}: ${accessory.displayName} Removing Leftover Faucet Service`); + accessory.removeService(this.Faucet.Service); } - accessory.removeService(this.faucetService!); } async removeFanService(accessory: PlatformAccessory): Promise { // If fanService still present, then remove first - this.fanService = this.accessory.getService(this.hap.Service.Fan); - if (this.fanService) { + if (this.Fan?.Service) { + this.Fan.Service = this.accessory.getService(this.hap.Service.Fanv2) as Service; this.warnLog(`${this.device.deviceType}: ${accessory.displayName} Removing Leftover Fan Service`); + accessory.removeService(this.Fan.Service); } - accessory.removeService(this.fanService!); } async removeWindowService(accessory: PlatformAccessory): Promise { // If windowService still present, then remove first - this.windowService = this.accessory.getService(this.hap.Service.Window); - if (this.windowService) { + if (this.Window?.Service) { + this.Window.Service = this.accessory.getService(this.hap.Service.Window) as Service; this.warnLog(`${this.device.deviceType}: ${accessory.displayName} Removing Leftover Window Service`); + accessory.removeService(this.Window.Service); } - accessory.removeService(this.windowService!); } async removeWindowCoveringService(accessory: PlatformAccessory): Promise { // If windowCoveringService still present, then remove first - this.windowCoveringService = this.accessory.getService(this.hap.Service.WindowCovering); - if (this.windowCoveringService) { + if (this.WindowCovering?.Service) { + this.WindowCovering.Service = this.accessory.getService(this.hap.Service.WindowCovering) as Service; this.warnLog(`${this.device.deviceType}: ${accessory.displayName} Removing Leftover Window Covering Service`); + accessory.removeService(this.WindowCovering.Service); } - accessory.removeService(this.windowCoveringService!); } async removeStatefulProgrammableSwitchService(accessory: PlatformAccessory): Promise { // If statefulProgrammableSwitchService still present, then remove first - this.statefulProgrammableSwitchService = this.accessory.getService(this.hap.Service.StatefulProgrammableSwitch); - if (this.statefulProgrammableSwitchService) { + if (this.StatefulProgrammableSwitch?.Service) { + this.StatefulProgrammableSwitch.Service = this.accessory.getService(this.hap.Service.StatefulProgrammableSwitch) as Service; this.warnLog(`${this.device.deviceType}: ${accessory.displayName} Removing Leftover Stateful Programmable Switch Service`); + accessory.removeService(this.StatefulProgrammableSwitch.Service); } - accessory.removeService(this.statefulProgrammableSwitchService!); } async removeSwitchService(accessory: PlatformAccessory): Promise { // If switchService still present, then remove first - this.switchService = this.accessory.getService(this.hap.Service.Switch); - if (this.switchService) { + if (this.Switch?.Service) { + this.Switch.Service = this.accessory.getService(this.hap.Service.Switch) as Service; this.warnLog(`${this.device.deviceType}: ${accessory.displayName} Removing Leftover Switch Service`); - } - accessory.removeService(this.switchService!); - } - - private DoublePress(device: device & devicesConfig) { - if (device.bot?.doublePress) { - this.doublePress = device.bot?.doublePress; - this.accessory.context.doublePress = this.doublePress; - } else { - this.doublePress = 1; - } - } - - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); - } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'H', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); - } - } - - async BLEPushConnection() { - if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Push Changes`); - await this.openAPIpushChanges(); - } - } - - async BLERefreshConnection(switchbot: any): Promise { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} wasn't able to establish BLE Connection, node-switchbot:` - + ` ${JSON.stringify(switchbot)}`); - if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Refresh Status`); - await this.openAPIRefreshStatus(); - } - } - - async retry({ max, fn }: { max: number; fn: { (): any; (): Promise } }): Promise { - return fn().catch(async (e: any) => { - if (max === 0) { - throw e; - } - this.infoLog(e); - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Retrying`); - await sleep(1000); - return this.retry({ max: max - 1, fn }); - }); - } - - maxRetry(): number { - if (this.device.maxRetry) { - return this.device.maxRetry; - } else { - return 5; + accessory.removeService(this.Switch.Service); } } - async PressOrSwitch(device: device & devicesConfig): Promise { - if (!device.bot?.mode) { - this.botMode = 'switch'; - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} does not have bot mode set in the Plugin's SwitchBot Device Settings,`); - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} is defaulting to "${this.botMode}" mode, you may experience issues.`); - } else if (device.bot?.mode === 'switch') { - this.botMode = 'switch'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Bot Mode: ${this.botMode}`); - } else if (device.bot?.mode === 'press') { - this.botMode = 'press'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Bot Mode: ${this.botMode}`); - } else if (device.bot?.mode === 'multipress') { - this.botMode = 'multipress'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Bot Mode: ${this.botMode}`); - } else { - throw new Error(`${this.device.deviceType}: ${this.accessory.displayName} Bot Mode: ${this.botMode}`); - } - } - - async allowPushChanges(device: device & devicesConfig): Promise { - if (device.bot?.allowPush) { - this.allowPush = true; + async getOn(): Promise { + let On: boolean; + if (this.botDeviceType === 'garagedoor') { + On = this.GarageDoor?.On ? true : false; + } else if (this.botDeviceType === 'door') { + On = this.Door?.On ? true : false; + } else if (this.botDeviceType === 'window') { + On = this.Window?.On ? true : false; + } else if (this.botDeviceType === 'windowcovering') { + On = this.WindowCovering?.On ? true : false; + } else if (this.botDeviceType === 'lock') { + On = this.LockMechanism?.On ? true : false; + } else if (this.botDeviceType === 'faucet') { + On = this.Faucet?.On ? true : false; + } else if (this.botDeviceType === 'fan') { + On = this.Fan?.On ? true : false; + } else if (this.botDeviceType === 'stateful') { + On = this.StatefulProgrammableSwitch?.On ? true : false; + } else if (this.botDeviceType === 'switch') { + On = this.Switch?.On ? true : false; } else { - this.allowPush = false; + On = this.Outlet?.On ? true : false; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Allowing Push Changes: ${this.allowPush}`); + return On; } - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - - async offlineOff(): Promise { - if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); - } - } - - async apiError(e: any): Promise { + async setOn(On: boolean): Promise { if (this.botDeviceType === 'garagedoor') { - this.garageDoorService?.updateCharacteristic(this.hap.Characteristic.TargetDoorState, e); - this.garageDoorService?.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, e); - this.garageDoorService?.updateCharacteristic(this.hap.Characteristic.ObstructionDetected, e); + if (this.GarageDoor) { + this.GarageDoor.On = On; + } } else if (this.botDeviceType === 'door') { - this.doorService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); - this.doorService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); - this.doorService?.updateCharacteristic(this.hap.Characteristic.PositionState, e); + if (this.Door) { + this.Door.On = On; + } } else if (this.botDeviceType === 'window') { - this.windowService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); - this.windowService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); - this.windowService?.updateCharacteristic(this.hap.Characteristic.PositionState, e); + if (this.Window) { + this.Window.On = On; + } } else if (this.botDeviceType === 'windowcovering') { - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.PositionState, e); + if (this.WindowCovering) { + this.WindowCovering.On = On; + } } else if (this.botDeviceType === 'lock') { - this.doorService?.updateCharacteristic(this.hap.Characteristic.LockTargetState, e); - this.doorService?.updateCharacteristic(this.hap.Characteristic.LockCurrentState, e); + if (this.LockMechanism) { + this.LockMechanism.On = On; + } } else if (this.botDeviceType === 'faucet') { - this.faucetService?.updateCharacteristic(this.hap.Characteristic.Active, e); + if (this.Faucet) { + this.Faucet.On = On; + } } else if (this.botDeviceType === 'fan') { - this.fanService?.updateCharacteristic(this.hap.Characteristic.On, e); + if (this.Fan) { + this.Fan.On = On; + } } else if (this.botDeviceType === 'stateful') { - this.statefulProgrammableSwitchService?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, e); - this.statefulProgrammableSwitchService?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, e); + if (this.StatefulProgrammableSwitch) { + this.StatefulProgrammableSwitch.On = On; + } } else if (this.botDeviceType === 'switch') { - this.switchService?.updateCharacteristic(this.hap.Characteristic.On, e); + if (this.Switch) { + this.Switch.On = On; + } } else { - this.outletService?.updateCharacteristic(this.hap.Characteristic.On, e); + if (this.Outlet) { + this.Outlet.On = On; + } } - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); } - async deviceType(device: device & devicesConfig): Promise { + async getBotConfigSettings(device: device & devicesConfig) { + //Bot Device Type if (!device.bot?.deviceType && this.accessory.context.deviceType) { this.botDeviceType = this.accessory.context.deviceType; this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Type: ${this.botDeviceType}, from Accessory Cache.`); @@ -1435,41 +1330,33 @@ export class Bot { this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} No Device Type Set, deviceType: ${this.device.bot?.deviceType}`); this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using default deviceType: ${this.botDeviceType}`); } - } - - async deviceContext() { - if (this.On === undefined) { - this.On = false; - this.accessory.context.On = this.On; - } else { - this.On = this.accessory.context.On; - } - if (this.BatteryLevel === undefined) { - this.BatteryLevel = 100; + // Bot Mode + if (!device.bot?.mode) { + this.botMode = 'switch'; + this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} does not have bot mode set in the Plugin's SwitchBot Device Settings,`); + this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} is defaulting to "${this.botMode}" mode, you may experience issues.`); + } else if (device.bot?.mode === 'switch') { + this.botMode = 'switch'; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Bot Mode: ${this.botMode}`); + } else if (device.bot?.mode === 'press') { + this.botMode = 'press'; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Bot Mode: ${this.botMode}`); + } else if (device.bot?.mode === 'multipress') { + this.botMode = 'multipress'; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Bot Mode: ${this.botMode}`); } else { - this.BatteryLevel = this.accessory.context.BatteryLevel; + throw new Error(`${this.device.deviceType}: ${this.accessory.displayName} Bot Mode: ${this.botMode}`); } - if (this.StatusLowBattery === undefined) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - this.accessory.context.StatusLowBattery = this.StatusLowBattery; + + // Bot Double Press + if (device.bot?.doublePress) { + this.doublePress = device.bot?.doublePress; + this.accessory.context.doublePress = this.doublePress; } else { - this.StatusLowBattery = this.accessory.context.StatusLowBattery; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + this.doublePress = 1; } - } - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - // pushRatePress + // Bot Press PushRate if (device?.bot?.pushRatePress) { this.pushRatePress = device?.bot?.pushRatePress; this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Bot pushRatePress: ${this.pushRatePress}`); @@ -1477,106 +1364,144 @@ export class Bot { this.pushRatePress = 15; this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default Bot pushRatePress: ${this.pushRatePress}`); } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.bot) { - config = device.bot; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.offline !== undefined) { - config['offline'] = device.offline; - } - if (device.maxRetry !== undefined) { - config['maxRetry'] = device.maxRetry; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); + // Bot Allow Push + if (device.bot?.allowPush) { + this.allowPush = true; } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); + this.allowPush = false; } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Allowing Push Changes: ${this.allowPush}`); + // Bot Multi Press Count + this.multiPressCount = 0; } - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); + async BLEPushConnection() { + if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') { + this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Push Changes`); + await this.openAPIpushChanges(); } } - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); + async BLERefreshConnection(switchbot: any): Promise { + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} wasn't able to establish BLE Connection, node-switchbot:` + + ` ${JSON.stringify(switchbot)}`); + if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') { + this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Refresh Status`); + await this.openAPIRefreshStatus(); } } - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); + async offlineOff(): Promise { + if (this.device.offline) { + if (this.botDeviceType === 'garagedoor') { + if (this.GarageDoor) { + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.hap.Characteristic.TargetDoorState.CLOSED); + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.CLOSED); + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.ObstructionDetected, false); + } + } else if (this.botDeviceType === 'door') { + if (this.Door) { + this.Door.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); + this.Door.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); + this.Door.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + } + } else if (this.botDeviceType === 'window') { + if (this.Window) { + this.Window.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); + this.Window.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); + this.Window.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + } + } else if (this.botDeviceType === 'windowcovering') { + if (this.WindowCovering) { + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + } + } else if (this.botDeviceType === 'lock') { + if (this.LockMechanism) { + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockTargetState, this.hap.Characteristic.LockTargetState.SECURED); + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, this.hap.Characteristic.LockCurrentState.SECURED); + } + } else if (this.botDeviceType === 'faucet') { + if (this.Faucet) { + this.Faucet.Service.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.INACTIVE); + } + } else if (this.botDeviceType === 'fan') { + if (this.Fan) { + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.On, false); + } + } else if (this.botDeviceType === 'stateful') { + if (this.StatefulProgrammableSwitch) { + this.StatefulProgrammableSwitch.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, + this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS); + this.StatefulProgrammableSwitch.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, 0); + } + } else if (this.botDeviceType === 'switch') { + if (this.Switch) { + this.Switch.Service.updateCharacteristic(this.hap.Characteristic.On, false); + } + } else { + if (this.Outlet) { + this.Outlet.Service.updateCharacteristic(this.hap.Characteristic.On, false); + } } } } - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); + async apiError(e: any): Promise { + if (this.botDeviceType === 'garagedoor') { + if (this.GarageDoor) { + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, e); + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, e); + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.ObstructionDetected, e); } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); + } else if (this.botDeviceType === 'door') { + if (this.Door) { + this.Door.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); + this.Door.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); + this.Door.Service.updateCharacteristic(this.hap.Characteristic.PositionState, e); + } + } else if (this.botDeviceType === 'window') { + if (this.Window) { + this.Window.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); + this.Window.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); + this.Window.Service.updateCharacteristic(this.hap.Characteristic.PositionState, e); + } + } else if (this.botDeviceType === 'windowcovering') { + if (this.WindowCovering) { + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.PositionState, e); + } + } else if (this.botDeviceType === 'lock') { + if (this.LockMechanism) { + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockTargetState, e); + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, e); + } + } else if (this.botDeviceType === 'faucet') { + if (this.Faucet) { + this.Faucet.Service.updateCharacteristic(this.hap.Characteristic.Active, e); + } + } else if (this.botDeviceType === 'fan') { + if (this.Fan) { + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.On, e); + } + } else if (this.botDeviceType === 'stateful') { + if (this.StatefulProgrammableSwitch) { + this.StatefulProgrammableSwitch.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, e); + this.StatefulProgrammableSwitch.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, e); + } + } else if (this.botDeviceType === 'switch') { + if (this.Switch) { + this.Switch.Service.updateCharacteristic(this.hap.Characteristic.On, e); + } + } else { + if (this.Outlet) { + this.Outlet.Service.updateCharacteristic(this.hap.Characteristic.On, e); } } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); } } diff --git a/src/device/ceilinglight.ts b/src/device/ceilinglight.ts index a720d1d3..d73e7348 100644 --- a/src/device/ceilinglight.ts +++ b/src/device/ceilinglight.ts @@ -1,143 +1,112 @@ +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * ceilinglight.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { sleep } from '../utils.js'; +import { deviceBase } from './device.js'; import { interval, Subject } from 'rxjs'; -import { SwitchBotPlatform } from '../platform.js'; +import { Devices } from '../settings.js'; +import { hs2rgb, rgb2hs, m2hs } from '../utils.js'; import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { device, devicesConfig, deviceStatus, hs2rgb, rgb2hs, m2hs, serviceData, Devices, SwitchBotPlatformConfig } from '../settings.js'; -import { - Service, PlatformAccessory, CharacteristicValue, ControllerConstructor, Controller, ControllerServiceMap, API, Logging, HAP, -} from 'homebridge'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; +import type { Service, PlatformAccessory, CharacteristicValue, ControllerConstructor, Controller, ControllerServiceMap } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class CeilingLight { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class CeilingLight extends deviceBase { // Services - lightBulbService!: Service; - - // Characteristic Values - On!: CharacteristicValue; - Hue!: CharacteristicValue; - Saturation!: CharacteristicValue; - Brightness!: CharacteristicValue; - ColorTemperature?: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - - // OpenAPI Status - OpenAPI_On: deviceStatus['power']; - OpenAPI_RGB: deviceStatus['color']; - OpenAPI_Brightness: deviceStatus['brightness']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_ColorTemperature?: deviceStatus['colorTemperature']; - - // BLE Status - BLE_Delay: serviceData['delay']; - BLE_On: serviceData['state']; - BLE_WifiRssi: serviceData['wifiRssi']; - - // BLE Others - BLE_IsConnected?: boolean; - - // Config - set_minStep?: number; - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; - adaptiveLightingShift?: number; + private LightBulb: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + Hue: CharacteristicValue; + Saturation: CharacteristicValue; + Brightness: CharacteristicValue; + ColorTemperature?: CharacteristicValue; + }; // Adaptive Lighting AdaptiveLightingController?: ControllerConstructor | Controller; - minKelvin!: number; - maxKelvin!: number; - - // Others - cacheKelvin!: number; + adaptiveLightingShift?: number; // Updates ceilingLightUpdateInProgress!: boolean; doCeilingLightUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; + super(platform, accessory, device); // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); this.adaptiveLighting(device); - this.deviceContext(); - this.deviceConfig(device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doCeilingLightUpdate = new Subject(); this.ceilingLightUpdateInProgress = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, this.model(device)) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Lightbulb service if it exists, otherwise create a new Lightbulb service - // you can create multiple services for each accessory - const lightBulbService = `${accessory.displayName} ${device.deviceType}`; - (this.lightBulbService = accessory.getService(this.hap.Service.Lightbulb) - || accessory.addService(this.hap.Service.Lightbulb)), lightBulbService; - - if (this.adaptiveLightingShift === -1 && this.accessory.context.adaptiveLighting) { - this.accessory.removeService(this.lightBulbService); - this.lightBulbService = this.accessory.addService(this.hap.Service.Lightbulb); - this.accessory.context.adaptiveLighting = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} adaptiveLighting: ${this.accessory.context.adaptiveLighting}`); + // Initialize LightBulb Service + accessory.context.LightBulb = accessory.context.LightBulb ?? {}; + this.LightBulb = { + Name: accessory.context.LightBul.bName ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Lightbulb) ?? accessory.addService(this.hap.Service.Lightbulb) as Service, + On: accessory.context.On ?? false, + Hue: accessory.context.Hue ?? 0, + Saturation: accessory.context.Saturation ?? 0, + Brightness: accessory.context.Brightness ?? 0, + ColorTemperature: accessory.context.ColorTemperature ?? 140, + }; + accessory.context.LightBulb = this.LightBulb as object; + + // Adaptive Lighting + if (this.adaptiveLightingShift === -1 && accessory.context.adaptiveLighting) { + accessory.removeService(this.LightBulb.Service); + this.LightBulb.Service = accessory.addService(this.hap.Service.Lightbulb); + accessory.context.adaptiveLighting = false; + this.debugLog(`${device.deviceType}: ${accessory.displayName} adaptiveLighting: ${accessory.context.adaptiveLighting}`); } - - this.lightBulbService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.lightBulbService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.lightBulbService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); + if (this.adaptiveLightingShift !== -1) { + this.AdaptiveLightingController = new platform.api.hap.AdaptiveLightingController(this.LightBulb.Service, { + customTemperatureAdjustment: this.adaptiveLightingShift, + }); + this.accessory.configureController(this.AdaptiveLightingController); + this.accessory.context.adaptiveLighting = true; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} adaptiveLighting: ${this.accessory.context.adaptiveLighting},` + + ` adaptiveLightingShift: ${this.adaptiveLightingShift}`); } - // handle on / off events using the On characteristic - this.lightBulbService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} adaptiveLightingShift: ${this.adaptiveLightingShift}`); - // handle Brightness events using the Brightness characteristic - this.lightBulbService + // Initialize LightBulb Characteristics + this.LightBulb.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LightBulb.Name) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.LightBulb.On; + }) + .onSet(this.OnSet.bind(this)); + + // Initialize LightBulb Brightness + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.Brightness) .setProps({ - minStep: this.minStep(device), + minStep: device.ceilinglight?.set_minStep ?? 1, minValue: 0, maxValue: 100, validValueRanges: [0, 100], }) .onGet(() => { - return this.Brightness; + return this.LightBulb.Brightness; }) .onSet(this.BrightnessSet.bind(this)); - // handle ColorTemperature events using the ColorTemperature characteristic - this.lightBulbService + // Initialize LightBulb ColorTemperature + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.ColorTemperature) .setProps({ minValue: 140, @@ -145,12 +114,12 @@ export class CeilingLight { validValueRanges: [140, 500], }) .onGet(() => { - return this.ColorTemperature!; + return this.LightBulb.ColorTemperature!; }) .onSet(this.ColorTemperatureSet.bind(this)); - // handle Hue events using the Hue characteristic - this.lightBulbService + // Initialize LightBulb Hue + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.Hue) .setProps({ minValue: 0, @@ -158,12 +127,12 @@ export class CeilingLight { validValueRanges: [0, 360], }) .onGet(() => { - return this.Hue; + return this.LightBulb.Hue; }) .onSet(this.HueSet.bind(this)); - // handle Hue events using the Hue characteristic - this.lightBulbService + // Initialize LightBulb Saturation + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.Saturation) .setProps({ minValue: 0, @@ -171,22 +140,12 @@ export class CeilingLight { validValueRanges: [0, 100], }) .onGet(() => { - return this.Saturation; + return this.LightBulb.Saturation; }) .onSet(this.SaturationSet.bind(this)); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} adaptiveLightingShift: ${this.adaptiveLightingShift}`); - if (this.adaptiveLightingShift !== -1) { - this.AdaptiveLightingController = new platform.api.hap.AdaptiveLightingController(this.lightBulbService, { - customTemperatureAdjustment: this.adaptiveLightingShift, - }); - this.accessory.configureController(this.AdaptiveLightingController); - this.accessory.context.adaptiveLighting = true; - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} adaptiveLighting: ${this.accessory.context.adaptiveLighting},` + - ` adaptiveLightingShift: ${this.adaptiveLightingShift}`, - ); - } + // Retrieve initial values and updateHomekit + this.refreshStatus(); // Update Homekit this.updateHomeKitCharacteristics(); @@ -199,27 +158,7 @@ export class CeilingLight { }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - const { powerState, brightness, colorTemperature } = context; - const { On, Brightness, ColorTemperature } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(powerState, brightness, colorTemperature) = ' + - `Webhook:(${powerState}, ${brightness}, ${colorTemperature}), ` + - `current:(${On}, ${Brightness}, ${ColorTemperature})`); - this.On = powerState === 'ON' ? true : false; - this.Brightness = brightness; - this.ColorTemperature = colorTemperature; - this.updateHomeKitCharacteristics(); - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); // Watch for Bulb change events // We put in a debounce of 100ms so we don't make duplicate calls @@ -228,117 +167,100 @@ export class CeilingLight { tap(() => { this.ceilingLightUpdateInProgress = true; }), - debounceTime(this.platform.config.options!.pushRate! * 1000), + debounceTime(this.devicePushRate * 1000), ) .subscribe(async () => { try { await this.pushChanges(); } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + - ` Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + + ` Error Message: ${JSON.stringify(e.message)}`); } this.ceilingLightUpdateInProgress = false; }); } - private model(device): CharacteristicValue { - let model: string; - if (device.deviceType === 'Ceiling Light') { - model = 'W2612230' || 'W2612240'; - } else if (device.deviceType === 'Ceiling Light Pro') { - model = 'W2612210' || 'W2612220'; - } else { - model = 'unknown'; - } - return model; - } - /** - * Parse the device status from the SwitchBot api + * Parse the device status from the SwitchBotBLE API */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } - /*if (this.BLE) { - await this.BLEparseStatus(); - } else*/ if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // State - switch (this.BLE_On) { + switch (serviceData.state) { case 'on': - this.On = true; + this.LightBulb.On = true; break; default: - this.On = false; + this.LightBulb.On = false; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); } - async openAPIparseStatus() { + /** + * Parse the device status from the SwitchBot OpenAPI + */ + async openAPIparseStatus(deviceStatus: deviceStatus) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); - switch (this.OpenAPI_On) { + switch (deviceStatus.body.power) { case 'on': - this.On = true; + this.LightBulb.On = true; break; default: - this.On = false; + this.LightBulb.On = false; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); // Brightness - this.Brightness = Number(this.OpenAPI_Brightness); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.Brightness}`); + this.LightBulb.Brightness = Number(deviceStatus.body.brightness); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); // Color, Hue & Brightness - if (this.OpenAPI_RGB) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} color: ${JSON.stringify(this.OpenAPI_RGB)}`); - const [red, green, blue] = this.OpenAPI_RGB!.split(':'); + if (deviceStatus.body.color) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} color: ${JSON.stringify(deviceStatus.body.color)}`); + const [red, green, blue] = deviceStatus.body.color!.split(':'); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} red: ${JSON.stringify(red)}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} green: ${JSON.stringify(green)}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} blue: ${JSON.stringify(blue)}`); const [hue, saturation] = rgb2hs(Number(red), Number(green), Number(blue)); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + ` hs: ${JSON.stringify(rgb2hs(Number(red), Number(green), Number(blue)))}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` hs: ${JSON.stringify(rgb2hs(Number(red), Number(green), Number(blue)))}`); // Hue - this.Hue = hue; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.Hue}`); + this.LightBulb.Hue = hue; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.LightBulb.Hue}`); // Saturation - this.Saturation = saturation; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.Saturation}`); + this.LightBulb.Saturation = saturation; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); } // ColorTemperature - if (!Number.isNaN(this.OpenAPI_ColorTemperature)) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} OpenAPI ColorTemperature: ${this.OpenAPI_ColorTemperature}`); - const mired = Math.round(1000000 / this.OpenAPI_ColorTemperature!); + if (!Number.isNaN(deviceStatus.body.colorTemperature)) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} OpenAPI ColorTemperature: ${deviceStatus.body.colorTemperature}`); + const mired = Math.round(1000000 / deviceStatus.body.colorTemperature!); - this.ColorTemperature = Number(mired); + this.LightBulb.ColorTemperature = Number(mired); - this.ColorTemperature = Math.max(Math.min(this.ColorTemperature, 500), 140); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.ColorTemperature}`); + this.LightBulb.ColorTemperature = Math.max(Math.min(this.LightBulb.ColorTemperature, 500), 140); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); } - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); + } } /** @@ -353,10 +275,8 @@ export class CeilingLight { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -373,110 +293,43 @@ export class CeilingLight { // Start to monitor advertisement packets (async () => { // Start to monitor advertisement packets - await switchbot.startScan({ - id: this.device.bleMac, - }); + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); - if (this.device.bleMac === ad.address && ad.model === 's') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_On = ad.serviceData.state; - //this.delay = ad.serviceData.delay; - //this.timer = ad.serviceData.timer; - //this.syncUtcTime = ad.serviceData.syncUtcTime; - //this.wifiRssi = ad.serviceData.wifiRssi; - //this.overload = ad.serviceData.overload; - //this.currentPower = ad.serviceData.currentPower; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: '', - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_On = ad.serviceData.state; - //this.delay = ad.serviceData.delay; - //this.timer = ad.serviceData.timer; - //this.syncUtcTime = ad.serviceData.syncUtcTime; - //this.wifiRssi = ad.serviceData.wifiRssi; - //this.overload = ad.serviceData.overload; - //this.currentPower = ad.serviceData.currentPower; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} state: ${ad.serviceData.state}, ` + - `delay: ${ad.serviceData.delay}, timer: ${ad.serviceData.timer}, syncUtcTime: ${ad.serviceData.syncUtcTime} ` + - `wifiRssi: ${ad.serviceData.wifiRssi}, overload: ${ad.serviceData.overload}, currentPower: ${ad.serviceData.currentPower}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } - async openAPIRefreshStatus() { + async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_On = deviceStatus.body.power; - this.OpenAPI_RGB = deviceStatus.body.color; - this.OpenAPI_Brightness = deviceStatus.body.brightness; - this.OpenAPI_ColorTemperature = deviceStatus.body.colorTemperature; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -484,10 +337,55 @@ export class CeilingLight { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { powerState, brightness, colorTemperature } = context; + const { On, Brightness, ColorTemperature } = this.LightBulb; + this.debugLog(`${device.deviceType}: ${accessory.displayName} ` + + '(powerState, brightness, colorTemperature) = ' + + `Webhook:(${powerState}, ${brightness}, ${colorTemperature}), ` + + `current:(${On}, ${Brightness}, ${ColorTemperature})`); + + // On + this.LightBulb.On = powerState === 'ON' ? true : false; + if (accessory.context.Brightness !== this.LightBulb.On) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} On: ${this.LightBulb.On}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} On: ${this.LightBulb.On}`); + } + + // Brightness + this.LightBulb.Brightness = brightness; + if (accessory.context.Brightness !== this.LightBulb.Brightness) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); + } + + // ColorTemperature + this.LightBulb.ColorTemperature = colorTemperature; + if (accessory.context.ColorTemperature !== this.LightBulb.ColorTemperature) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); + } + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} ` + + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -511,9 +409,8 @@ export class CeilingLight { await this.openAPIpushChanges(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, pushChanges will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, pushChanges will not happen.`); } // Refresh the status from the API interval(15000) @@ -526,8 +423,9 @@ export class CeilingLight { async BLEpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`); - if (this.On !== this.accessory.context.On) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges On: ${this.On} OnCached: ${this.accessory.context.On}`); + if (this.LightBulb.On !== this.accessory.context.On) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` BLEpushChanges On: ${this.LightBulb.On} OnCached: ${this.accessory.context.On}`); const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -541,11 +439,11 @@ export class CeilingLight { id: this.device.bleMac, }) .then(async (device_list: any) => { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); - return await this.retry({ - max: this.maxRetry(), + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); + return await this.retryBLE({ + max: await this.maxRetryBLE(), fn: async () => { - if (this.On) { + if (this.LightBulb.On) { return await device_list[0].turnOn({ id: this.device.bleMac }); } else { return await device_list[0].turnOff({ id: this.device.bleMac }); @@ -555,28 +453,28 @@ export class CeilingLight { }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.On = false; + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `On: ${this.LightBulb.On} sent over BLE, sent successfully`); + this.LightBulb.On = false; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges.` + `On: ${this.On}, ` + `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` No BLEpushChanges: On: ${this.LightBulb.On}, ` + + `OnCached: ${this.accessory.context.On}`); } } async openAPIpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`); - if (this.On !== this.accessory.context.On) { + if (this.LightBulb.On !== this.accessory.context.On) { let command = ''; - if (this.On) { + if (this.LightBulb.On) { command = 'turnOn'; } else { command = 'turnOff'; @@ -599,46 +497,44 @@ export class CeilingLight { this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus body: ${JSON.stringify(deviceStatus.body)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.` + - `On: ${this.On}, ` + - `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.` + + `On: ${this.LightBulb.On}, ` + + `OnCached: ${this.accessory.context.On}`); } // Push Hue & Saturation Update - if (this.On) { + if (this.LightBulb.On) { await this.pushHueSaturationChanges(); } // Push ColorTemperature Update - if (this.On) { + if (this.LightBulb.On) { await this.pushColorTemperatureChanges(); } // Push Brightness Update - if (this.On) { + if (this.LightBulb.On) { await this.pushBrightnessChanges(); } } async pushHueSaturationChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushHueSaturationChanges`); - if (this.Hue !== this.accessory.context.Hue || this.Saturation !== this.accessory.context.Saturation) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${JSON.stringify(this.Hue)}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${JSON.stringify(this.Saturation)}`); - const [red, green, blue] = hs2rgb(Number(this.Hue), Number(this.Saturation)); + if (this.LightBulb.Hue !== this.accessory.context.Hue || this.LightBulb.Saturation !== this.accessory.context.Saturation) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${JSON.stringify(this.LightBulb.Hue)}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${JSON.stringify(this.LightBulb.Saturation)}`); + const [red, green, blue] = hs2rgb(Number(this.LightBulb.Hue), Number(this.LightBulb.Saturation)); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} rgb: ${JSON.stringify([red, green, blue])}`); const bodyChange = JSON.stringify({ command: 'setColor', @@ -658,32 +554,30 @@ export class CeilingLight { this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus body: ${JSON.stringify(deviceStatus.body)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushHueSaturationChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushHueSaturationChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No pushHueSaturationChanges. Hue: ${this.Hue}, ` + - `HueCached: ${this.accessory.context.Hue}, Saturation: ${this.Saturation}, SaturationCached: ${this.accessory.context.Saturation}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushHueSaturationChanges. Hue: ${this.LightBulb.Hue}, HueCached: ` + + `${this.accessory.context.Hue}, Saturation: ${this.LightBulb.Saturation}, SaturationCached: ${this.accessory.context.Saturation}`); } } async pushColorTemperatureChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushColorTemperatureChanges`); - if (this.ColorTemperature !== this.accessory.context.ColorTemperature) { - const kelvin = Math.round(1000000 / Number(this.ColorTemperature)); - this.cacheKelvin = kelvin; + if (this.LightBulb.ColorTemperature !== this.accessory.context.ColorTemperature) { + const kelvin = Math.round(1000000 / Number(this.LightBulb.ColorTemperature)); + this.accessory.context.kelvin = kelvin; const bodyChange = JSON.stringify({ command: 'setColorTemperature', parameter: `${kelvin}`, @@ -702,7 +596,7 @@ export class CeilingLight { this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus body: ${JSON.stringify(deviceStatus.body)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); } else { this.statusCode(statusCode); @@ -710,25 +604,21 @@ export class CeilingLight { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushColorTemperatureChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushColorTemperatureChanges with` + + ` ${this.device.connectionType} Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No pushColorTemperatureChanges.` + - `ColorTemperature: ${this.ColorTemperature}, ColorTemperatureCached: ${this.accessory.context.ColorTemperature}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushColorTemperatureChanges.` + + `ColorTemperature: ${this.LightBulb.ColorTemperature}, ColorTemperatureCached: ${this.accessory.context.ColorTemperature}`); } } async pushBrightnessChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushBrightnessChanges`); - if (this.Brightness !== this.accessory.context.Brightness) { + if (this.LightBulb.Brightness !== this.accessory.context.Brightness) { const bodyChange = JSON.stringify({ command: 'setBrightness', - parameter: `${this.Brightness}`, + parameter: `${this.LightBulb.Brightness}`, commandType: 'command', }); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`); @@ -744,25 +634,23 @@ export class CeilingLight { this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus body: ${JSON.stringify(deviceStatus.body)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushBrightnessChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushBrightnessChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No pushBrightnessChanges.` + - `Brightness: ${this.Brightness}, ` + - `BrightnessCached: ${this.accessory.context.Brightness}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushBrightnessChanges.` + + `Brightness: ${this.LightBulb.Brightness}, ` + + `BrightnessCached: ${this.accessory.context.Brightness}`); } } @@ -770,13 +658,13 @@ export class CeilingLight { * Handle requests to set the value of the "On" characteristic */ async OnSet(value: CharacteristicValue): Promise { - if (this.On === this.accessory.context.On) { + if (this.LightBulb.On === this.accessory.context.On) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set On: ${value}`); } else { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set On: ${value}`); } - this.On = value; + this.LightBulb.On = value; this.doCeilingLightUpdate.next(); } @@ -784,15 +672,15 @@ export class CeilingLight { * Handle requests to set the value of the "Brightness" characteristic */ async BrightnessSet(value: CharacteristicValue): Promise { - if (this.Brightness === this.accessory.context.Brightness) { + if (this.LightBulb.Brightness === this.accessory.context.Brightness) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Brightness: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`); } - this.Brightness = value; + this.LightBulb.Brightness = value; this.doCeilingLightUpdate.next(); } @@ -800,30 +688,32 @@ export class CeilingLight { * Handle requests to set the value of the "ColorTemperature" characteristic */ async ColorTemperatureSet(value: CharacteristicValue): Promise { - if (this.ColorTemperature === this.accessory.context.ColorTemperature) { + if (this.LightBulb.ColorTemperature === this.accessory.context.ColorTemperature) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set ColorTemperature: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set ColorTemperature: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set ColorTemperature: ${value}`); } + const minKelvin = 2000; + const maxKelvin = 9000; // Convert mired to kelvin to nearest 100 (SwitchBot seems to need this) const kelvin = Math.round(1000000 / Number(value) / 100) * 100; // Check and increase/decrease kelvin to range of device - const k = Math.min(Math.max(kelvin, this.minKelvin), this.maxKelvin); + const k = Math.min(Math.max(kelvin, minKelvin), maxKelvin); - if (!this.accessory.context.On || this.cacheKelvin === k) { + if (!this.accessory.context.On || this.accessory.context.kelvin === k) { return; } // Updating the hue/sat to the corresponding values mimics native adaptive lighting const hs = m2hs(value); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Hue, hs[0]); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Saturation, hs[1]); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Hue, hs[0]); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Saturation, hs[1]); - this.ColorTemperature = value; + this.LightBulb.ColorTemperature = value; this.doCeilingLightUpdate.next(); } @@ -831,17 +721,17 @@ export class CeilingLight { * Handle requests to set the value of the "Hue" characteristic */ async HueSet(value: CharacteristicValue): Promise { - if (this.Hue === this.accessory.context.Hue) { + if (this.LightBulb.Hue === this.accessory.context.Hue) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Hue: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Hue: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Hue: ${value}`); } - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); - this.Hue = value; + this.LightBulb.Hue = value; this.doCeilingLightUpdate.next(); } @@ -849,55 +739,56 @@ export class CeilingLight { * Handle requests to set the value of the "Saturation" characteristic */ async SaturationSet(value: CharacteristicValue): Promise { - if (this.Saturation === this.accessory.context.Saturation) { + if (this.LightBulb.Saturation === this.accessory.context.Saturation) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Saturation: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Saturation: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Saturation: ${value}`); } - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); - this.Saturation = value; + this.LightBulb.Saturation = value; this.doCeilingLightUpdate.next(); } async updateHomeKitCharacteristics(): Promise { - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.LightBulb.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); } else { - this.accessory.context.On = this.On; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + this.accessory.context.On = this.LightBulb.On; + this.LightBulb!.Service.updateCharacteristic(this.hap.Characteristic.On, this.LightBulb.On); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.LightBulb.On}`); } - if (this.Brightness === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.Brightness}`); + if (this.LightBulb.Brightness === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); } else { - this.accessory.context.Brightness = this.Brightness; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Brightness, this.Brightness); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Brightness: ${this.Brightness}`); + this.accessory.context.Brightness = this.LightBulb.Brightness; + this.LightBulb!.Service.updateCharacteristic(this.hap.Characteristic.Brightness, this.LightBulb.Brightness); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Brightness: ${this.LightBulb.Brightness}`); } - if (this.ColorTemperature === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.ColorTemperature}`); + if (this.LightBulb.ColorTemperature === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); } else { - this.accessory.context.ColorTemperature = this.ColorTemperature; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.ColorTemperature, this.ColorTemperature); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic ColorTemperature: ${this.ColorTemperature}`); + this.accessory.context.ColorTemperature = this.LightBulb.ColorTemperature; + this.LightBulb!.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, this.LightBulb.ColorTemperature); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` updateCharacteristic ColorTemperature: ${this.LightBulb.ColorTemperature}`); } - if (this.Hue === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.Hue}`); + if (this.LightBulb.Hue === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.LightBulb.Hue}`); } else { - this.accessory.context.Hue = this.Hue; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Hue, this.Hue); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Hue: ${this.Hue}`); + this.accessory.context.Hue = this.LightBulb.Hue; + this.LightBulb!.Service.updateCharacteristic(this.hap.Characteristic.Hue, this.LightBulb.Hue); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Hue: ${this.LightBulb.Hue}`); } - if (this.Saturation === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.Saturation}`); + if (this.LightBulb.Saturation === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); } else { - this.accessory.context.Saturation = this.Saturation; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Saturation, this.Saturation); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Saturation: ${this.Saturation}`); + this.accessory.context.Saturation = this.LightBulb.Saturation; + this.LightBulb!.Service.updateCharacteristic(this.hap.Characteristic.Saturation, this.LightBulb.Saturation); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Saturation: ${this.LightBulb.Saturation}`); } } @@ -911,35 +802,6 @@ export class CeilingLight { } } - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); - } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} customBLEaddress: ${this.device.customBLEaddress}`); - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'u', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); - } - } - async BLEPushConnection() { if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') { this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Push Changes`); @@ -956,284 +818,17 @@ export class CeilingLight { } } - async retry({ max, fn }: { max: number; fn: { (): any; (): Promise } }): Promise { - return fn().catch(async (e: any) => { - if (max === 0) { - throw e; - } - this.infoLog(e); - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Retrying`); - await sleep(1000); - return this.retry({ max: max - 1, fn }); - }); - } - - maxRetry(): number { - if (this.device.maxRetry) { - return this.device.maxRetry; - } else { - return 5; - } - } - - minStep(device: device & devicesConfig): number { - if (device.ceilinglight?.set_minStep) { - this.set_minStep = device.ceilinglight?.set_minStep; - } else { - this.set_minStep = 1; - } - return this.set_minStep; - } - - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.On, false); } } apiError(e: any): void { - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.On, e); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Hue, e); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Brightness, e); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Saturation, e); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.ColorTemperature, e); - } - - async deviceContext() { - if (this.On === undefined) { - this.On = false; - } else { - this.On = this.accessory.context.On; - } - if (this.Hue === undefined) { - this.Hue = 0; - } else { - this.Hue = this.accessory.context.Hue; - } - if (this.Brightness === undefined) { - this.Brightness = 0; - } else { - this.Brightness = this.accessory.context.Brightness; - } - if (this.Brightness === undefined) { - this.Saturation = 0; - } else { - this.Saturation = this.accessory.context.Saturation; - } - if (this.ColorTemperature === undefined) { - this.ColorTemperature = 140; - } else { - this.ColorTemperature = this.accessory.context.ColorTemperature; - } - this.minKelvin = 2000; - this.maxKelvin = 9000; - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.ceilinglight) { - config = device.ceilinglight; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.offline !== undefined) { - config['offline'] = device.offline; - } - if (device.maxRetry !== undefined) { - config['maxRetry'] = device.maxRetry; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.LightBulb!.Service.updateCharacteristic(this.hap.Characteristic.On, e); + this.LightBulb!.Service.updateCharacteristic(this.hap.Characteristic.Hue, e); + this.LightBulb!.Service.updateCharacteristic(this.hap.Characteristic.Brightness, e); + this.LightBulb!.Service.updateCharacteristic(this.hap.Characteristic.Saturation, e); + this.LightBulb!.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, e); } } diff --git a/src/device/colorbulb.ts b/src/device/colorbulb.ts index fce61c14..7bf406c7 100644 --- a/src/device/colorbulb.ts +++ b/src/device/colorbulb.ts @@ -1,152 +1,110 @@ +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * blindtilt.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { sleep } from '../utils.js'; +import { deviceBase } from './device.js'; import { interval, Subject } from 'rxjs'; -import { SwitchBotPlatform } from '../platform.js'; +import { Devices } from '../settings.js'; +import { hs2rgb, rgb2hs, m2hs } from '../utils.js'; import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { device, devicesConfig, deviceStatus, hs2rgb, rgb2hs, m2hs, serviceData, Devices, SwitchBotPlatformConfig } from '../settings.js'; -import { - Service, PlatformAccessory, CharacteristicValue, ControllerConstructor, Controller, ControllerServiceMap, API, Logging, HAP, -} from 'homebridge'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { device, devicesConfig, deviceStatus, serviceData } from '../settings.js'; +import type { Service, PlatformAccessory, CharacteristicValue, ControllerConstructor, Controller, ControllerServiceMap } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class ColorBulb { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class ColorBulb extends deviceBase { // Services - lightBulbService!: Service; - - // Characteristic Values - On!: CharacteristicValue; - Hue!: CharacteristicValue; - Saturation!: CharacteristicValue; - Brightness!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - ColorTemperature?: CharacteristicValue; - - // OpenAPI Status - OpenAPI_On: deviceStatus['power']; - OpenAPI_RGB: deviceStatus['color']; - OpenAPI_Brightness: deviceStatus['brightness']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_ColorTemperature?: deviceStatus['colorTemperature']; - - // BLE Status - BLE_ColorTemperature: serviceData['color_temperature']; - BLE_Power: serviceData['power']; - BLE_Red: serviceData['red']; - BLE_Blue: serviceData['blue']; - BLE_Green: serviceData['green']; - BLE_On: serviceData['state']; - BLE_Delay: serviceData['delay']; - BLE_WifiRssi: serviceData['wifiRssi']; - BLE_Brightness: serviceData['brightness']; - BLE_Saturation; - BLE_Hue; - - // BLE Others - BLE_IsConnected?: boolean; - - // Config - set_minStep?: number; - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; - adaptiveLightingShift?: number; + private LightBulb: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + Hue: CharacteristicValue; + Saturation: CharacteristicValue; + Brightness: CharacteristicValue; + ColorTemperature?: CharacteristicValue; + }; // Adaptive Lighting AdaptiveLightingController?: ControllerConstructor | Controller; - minKelvin!: number; - maxKelvin!: number; - - // Others - cacheKelvin!: number; + adaptiveLightingShift?: number; // Updates colorBulbUpdateInProgress!: boolean; doColorBulbUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; + super(platform, accessory, device); // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); this.adaptiveLighting(device); - this.deviceContext(); - this.deviceConfig(device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doColorBulbUpdate = new Subject(); this.colorBulbUpdateInProgress = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, 'W1401400') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Lightbulb service if it exists, otherwise create a new Lightbulb service - // you can create multiple services for each accessory - const lightBulbService = `${accessory.displayName} ${device.deviceType}`; - (this.lightBulbService = accessory.getService(this.hap.Service.Lightbulb) - || accessory.addService(this.hap.Service.Lightbulb)), lightBulbService; - - if (this.adaptiveLightingShift === -1 && this.accessory.context.adaptiveLighting) { - this.accessory.removeService(this.lightBulbService); - this.lightBulbService = this.accessory.addService(this.hap.Service.Lightbulb); - this.accessory.context.adaptiveLighting = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} adaptiveLighting: ${this.accessory.context.adaptiveLighting}`); + // Initialize LightBulb property + accessory.context.LightBulb = accessory.context.LightBulb ?? {}; + this.LightBulb = { + Name: accessory.context.LightBulb.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Lightbulb) ?? accessory.addService(this.hap.Service.Lightbulb) as Service, + On: accessory.context.On ?? false, + Hue: accessory.context.Hue ?? 0, + Saturation: accessory.context.Saturation ?? 0, + Brightness: accessory.context.Brightness ?? 0, + ColorTemperature: accessory.context.ColorTemperature ?? 140, + }; + accessory.context.LightBulb = this.LightBulb as object; + + // Adaptive Lighting + if (this.adaptiveLightingShift === -1 && accessory.context.adaptiveLighting) { + accessory.removeService(this.LightBulb.Service); + this.LightBulb.Service = accessory.addService(this.hap.Service.Lightbulb); + accessory.context.adaptiveLighting = false; + this.debugLog(`${device.deviceType}: ${accessory.displayName} adaptiveLighting: ${accessory.context.adaptiveLighting}`); } - - this.lightBulbService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.lightBulbService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.lightBulbService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); + if (this.adaptiveLightingShift !== -1) { + this.AdaptiveLightingController = new platform.api.hap.AdaptiveLightingController(this.LightBulb.Service, { + customTemperatureAdjustment: this.adaptiveLightingShift, + }); + accessory.configureController(this.AdaptiveLightingController); + accessory.context.adaptiveLighting = true; + this.debugLog(`${device.deviceType}: ${this.accessory.displayName} adaptiveLighting: ${accessory.context.adaptiveLighting},` + + ` adaptiveLightingShift: ${this.adaptiveLightingShift}`); } + this.debugLog(`${device.deviceType}: ${this.accessory.displayName} adaptiveLightingShift: ${this.adaptiveLightingShift}`); - // handle on / off events using the On characteristic - this.lightBulbService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); + // Initialize LightBulb Characteristics + this.LightBulb.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LightBulb.Name) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.LightBulb.On; + }) + .onSet(this.OnSet.bind(this)); - // handle Brightness events using the Brightness characteristic - this.lightBulbService + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.Brightness) .setProps({ - minStep: this.minStep(device), + minStep: device.colorbulb?.set_minStep ?? 1, minValue: 0, maxValue: 100, validValueRanges: [0, 100], }) .onGet(() => { - return this.Brightness; + return this.LightBulb.Brightness; }) .onSet(this.BrightnessSet.bind(this)); - // handle ColorTemperature events using the ColorTemperature characteristic - this.lightBulbService + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.ColorTemperature) .setProps({ minValue: 140, @@ -154,12 +112,11 @@ export class ColorBulb { validValueRanges: [140, 500], }) .onGet(() => { - return this.ColorTemperature!; + return this.LightBulb.ColorTemperature!; }) .onSet(this.ColorTemperatureSet.bind(this)); - // handle Hue events using the Hue characteristic - this.lightBulbService + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.Hue) .setProps({ minValue: 0, @@ -167,12 +124,11 @@ export class ColorBulb { validValueRanges: [0, 360], }) .onGet(() => { - return this.Hue; + return this.LightBulb.Hue; }) .onSet(this.HueSet.bind(this)); - // handle Hue events using the Hue characteristic - this.lightBulbService + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.Saturation) .setProps({ minValue: 0, @@ -180,22 +136,12 @@ export class ColorBulb { validValueRanges: [0, 100], }) .onGet(() => { - return this.Saturation; + return this.LightBulb.Saturation; }) .onSet(this.SaturationSet.bind(this)); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} adaptiveLightingShift: ${this.adaptiveLightingShift}`); - if (this.adaptiveLightingShift !== -1) { - this.AdaptiveLightingController = new platform.api.hap.AdaptiveLightingController(this.lightBulbService, { - customTemperatureAdjustment: this.adaptiveLightingShift, - }); - this.accessory.configureController(this.AdaptiveLightingController); - this.accessory.context.adaptiveLighting = true; - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} adaptiveLighting: ${this.accessory.context.adaptiveLighting},` + - ` adaptiveLightingShift: ${this.adaptiveLightingShift}`, - ); - } + // Retrieve initial values and updateHomekit + this.refreshStatus(); // Update Homekit this.updateHomeKitCharacteristics(); @@ -208,60 +154,7 @@ export class ColorBulb { }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - const { powerState, brightness, color, colorTemperature } = context; - const { On, Brightness, Hue, Saturation, ColorTemperature } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(powerState, brightness, color, colorTemperature) = ' + - `Webhook:(${powerState}, ${brightness}, ${color}, ${colorTemperature}), ` + - `current:(${On}, ${Brightness}, ${Hue}, ${Saturation}, ${ColorTemperature})`); - this.On = powerState === 'ON' ? true : false; - this.Brightness = brightness; - - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} color: ${JSON.stringify(color)}`); - const [red, green, blue] = color!.split(':'); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} red: ${JSON.stringify(red)}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} green: ${JSON.stringify(green)}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} blue: ${JSON.stringify(blue)}`); - - const [hue, saturation] = rgb2hs(Number(red), Number(green), Number(blue)); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + ` hs: ${JSON.stringify(rgb2hs(Number(red), Number(green), Number(blue)))}`, - ); - - // Hue - this.Hue = hue; - if (this.accessory.context.Hue !== this.Hue) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.Hue}`); - } else { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.Hue}`); - } - - // Saturation - this.Saturation = saturation; - if (this.accessory.context.Saturation !== this.Saturation) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.Saturation}`); - } else { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.Saturation}`); - } - - this.ColorTemperature = colorTemperature; - if (this.accessory.context.ColorTemperature !== this.ColorTemperature) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.ColorTemperature}`); - } else { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.ColorTemperature}`); - } - this.updateHomeKitCharacteristics(); - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); // Watch for Bulb change events // We put in a debounce of 100ms so we don't make duplicate calls @@ -270,138 +163,130 @@ export class ColorBulb { tap(() => { this.colorBulbUpdateInProgress = true; }), - debounceTime(this.platform.config.options!.pushRate! * 1000), + debounceTime(this.devicePushRate * 1000), ) .subscribe(async () => { try { await this.pushChanges(); } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + - ` Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + + ` Error Message: ${JSON.stringify(e.message)}`); } this.colorBulbUpdateInProgress = false; }); } /** - * Parse the device status from the SwitchBot api + * Parse the device status from the SwitchBotBLE API */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // State - switch (this.BLE_On) { + switch (serviceData.power) { case true: - this.On = true; + this.LightBulb.On = true; break; default: - this.On = false; + this.LightBulb.On = false; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); // Brightness - this.Brightness = Number(this.BLE_Brightness); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.Brightness}`); + this.LightBulb.Brightness = Number(serviceData.brightness); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); // Color, Hue & Brightness - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} red: ${this.BLE_Red}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} green: ${this.BLE_Green}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} blue: ${this.BLE_Blue}`); - - const [hue, saturation] = rgb2hs(Number(this.BLE_Red), Number(this.BLE_Green), Number(this.BLE_Blue)); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` hs: ${JSON.stringify(rgb2hs(Number(this.BLE_Red), Number(this.BLE_Green), Number(this.BLE_Blue)))}`, - ); - this.BLE_Hue = hue; - this.BLE_Saturation = saturation; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} red: ${serviceData.red}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} green: ${serviceData.green}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} blue: ${serviceData.blue}`); + + const [hue, saturation] = rgb2hs(Number(serviceData.red), Number(serviceData.green), Number(serviceData.blue)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` hs: ${JSON.stringify(rgb2hs(Number(serviceData.red), Number(serviceData.green), Number(serviceData.blue)))}`); // Hue - this.Hue = this.BLE_Hue; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.Hue}`); + this.LightBulb.Hue = hue; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.LightBulb.Hue}`); // Saturation - this.Saturation = this.BLE_Saturation; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.Saturation}`); + this.LightBulb.Saturation = saturation; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); // ColorTemperature - if (this.BLE_ColorTemperature) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.BLE_ColorTemperature}`); - this.ColorTemperature = this.BLE_ColorTemperature!; + if (serviceData.color_temperature) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${serviceData.color_temperature}`); + this.LightBulb.ColorTemperature = serviceData.color_temperature!; - this.ColorTemperature = Math.max(Math.min(this.ColorTemperature, 500), 140); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.ColorTemperature}`); + this.LightBulb.ColorTemperature = Math.max(Math.min(this.LightBulb.ColorTemperature, 500), 140); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); } } - async openAPIparseStatus() { + /** + * Parse the device status from the SwitchBot OpenAPI + */ + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); - switch (this.OpenAPI_On) { + switch (deviceStatus.body.power) { case 'on': - this.On = true; + this.LightBulb.On = true; break; default: - this.On = false; + this.LightBulb.On = false; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); // Brightness - this.Brightness = Number(this.OpenAPI_Brightness); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.Brightness}`); + this.LightBulb.Brightness = Number(deviceStatus.body.brightness); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); // Color, Hue & Brightness - if (this.OpenAPI_RGB) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} color: ${JSON.stringify(this.OpenAPI_RGB)}`); - const [red, green, blue] = this.OpenAPI_RGB!.split(':'); + if (deviceStatus.body.color) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} color: ${JSON.stringify(deviceStatus.body.color)}`); + const [red, green, blue] = deviceStatus.body.color!.split(':'); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} red: ${JSON.stringify(red)}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} green: ${JSON.stringify(green)}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} blue: ${JSON.stringify(blue)}`); const [hue, saturation] = rgb2hs(Number(red), Number(green), Number(blue)); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + ` hs: ${JSON.stringify(rgb2hs(Number(red), Number(green), Number(blue)))}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` hs: ${JSON.stringify(rgb2hs(Number(red), Number(green), Number(blue)))}`); // Hue - this.Hue = hue; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.Hue}`); + this.LightBulb.Hue = hue; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.LightBulb.Hue}`); // Saturation - this.Saturation = saturation; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.Saturation}`); + this.LightBulb.Saturation = saturation; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); } // ColorTemperature - if (!Number.isNaN(this.OpenAPI_ColorTemperature)) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.OpenAPI_ColorTemperature}`); - const mired = Math.round(1000000 / this.OpenAPI_ColorTemperature!); + if (!Number.isNaN(deviceStatus.body.colorTemperature)) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${deviceStatus.body.colorTemperature}`); + const mired = Math.round(1000000 / deviceStatus.body.colorTemperature!); - this.ColorTemperature = Number(mired); + this.LightBulb.ColorTemperature = Number(mired); - this.ColorTemperature = Math.max(Math.min(this.ColorTemperature, 500), 140); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.ColorTemperature}`); + this.LightBulb.ColorTemperature = Math.max(Math.min(this.LightBulb.ColorTemperature, 500), 140); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); } - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); + } } /** @@ -416,10 +301,8 @@ export class ColorBulb { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -436,113 +319,43 @@ export class ColorBulb { // Start to monitor advertisement packets (async () => { // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'u', - id: this.device.bleMac, - }); + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === 'u') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_ColorTemperature = ad.serviceData.color_temperature; - this.BLE_Power = ad.serviceData.power; - this.BLE_On = ad.serviceData.state; - this.BLE_Red = ad.serviceData.red; - this.BLE_Green = ad.serviceData.green; - this.BLE_Blue = ad.serviceData.blue; - this.BLE_Brightness = ad.serviceData.brightness; - this.BLE_Delay = ad.serviceData.delay; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: 'u', - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_Power = ad.serviceData.power; - this.BLE_On = ad.serviceData.state; - this.BLE_Red = ad.serviceData.red; - this.BLE_Green = ad.serviceData.green; - this.BLE_Blue = ad.serviceData.blue; - this.BLE_ColorTemperature = ad.serviceData.color_temperature; - this.BLE_Brightness = ad.serviceData.brightness; - this.BLE_Delay = ad.serviceData.delay; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} state: ${ad.serviceData.state}, ` + - `delay: ${ad.serviceData.delay}, timer: ${ad.serviceData.timer}, syncUtcTime: ${ad.serviceData.syncUtcTime} ` + - `wifiRssi: ${ad.serviceData.wifiRssi}, overload: ${ad.serviceData.overload}, currentPower: ${ad.serviceData.currentPower}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } - async openAPIRefreshStatus() { + async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_On = deviceStatus.body.power; - this.OpenAPI_RGB = deviceStatus.body.color; - this.OpenAPI_Brightness = deviceStatus.body.brightness; - this.OpenAPI_ColorTemperature = deviceStatus.body.colorTemperature; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -550,10 +363,81 @@ export class ColorBulb { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { powerState, brightness, color, colorTemperature } = context; + const { On, Brightness, Hue, Saturation, ColorTemperature } = this.LightBulb; + this.debugLog(`${device.deviceType}: ${accessory.displayName} ` + + '(powerState, brightness, color, colorTemperature) = ' + + `Webhook:(${powerState}, ${brightness}, ${color}, ${colorTemperature}), ` + + `current:(${On}, ${Brightness}, ${Hue}, ${Saturation}, ${ColorTemperature})`); + + // On + this.LightBulb.On = powerState === 'ON' ? true : false; + if (accessory.context.Brightness !== this.LightBulb.On) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} On: ${this.LightBulb.On}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} On: ${this.LightBulb.On}`); + } + + // Brightness + this.LightBulb.Brightness = brightness; + if (accessory.context.Brightness !== this.LightBulb.Brightness) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); + } + + this.debugLog(`${device.deviceType}: ${accessory.displayName} color: ${JSON.stringify(color)}`); + const [red, green, blue] = color!.split(':'); + this.debugLog(`${device.deviceType}: ${accessory.displayName} red: ${JSON.stringify(red)}`); + this.debugLog(`${device.deviceType}: ${accessory.displayName} green: ${JSON.stringify(green)}`); + this.debugLog(`${device.deviceType}: ${accessory.displayName} blue: ${JSON.stringify(blue)}`); + + const [hue, saturation] = rgb2hs(Number(red), Number(green), Number(blue)); + this.debugLog( + `${device.deviceType}: ${accessory.displayName}` + + ` hs: ${JSON.stringify(rgb2hs(Number(red), Number(green), Number(blue)))}`); + + // Hue + this.LightBulb.Hue = hue; + if (accessory.context.Hue !== this.LightBulb.Hue) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} Hue: ${this.LightBulb.Hue}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} Hue: ${this.LightBulb.Hue}`); + } + + // Saturation + this.LightBulb.Saturation = saturation; + if (accessory.context.Saturation !== this.LightBulb.Saturation) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); + } + + this.LightBulb.ColorTemperature = colorTemperature; + if (accessory.context.ColorTemperature !== this.LightBulb.ColorTemperature) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); + } + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} ` + + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugWarnLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -571,15 +455,14 @@ export class ColorBulb { async pushChanges(): Promise { if (!this.device.enableCloudService && this.OpenAPI) { this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} pushChanges enableCloudService: ${this.device.enableCloudService}`); - /*} else if (this.BLE) { - await this.BLEpushChanges();*/ + } else if (this.BLE) { + await this.BLEpushChanges(); } else if (this.OpenAPI && this.platform.config.credentials?.token) { await this.openAPIpushChanges(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, pushChanges will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, pushChanges will not happen.`); } // Refresh the status from the API interval(15000) @@ -592,8 +475,9 @@ export class ColorBulb { async BLEpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`); - if (this.On !== this.accessory.context.On) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges On: ${this.On} OnCached: ${this.accessory.context.On}`); + if (this.LightBulb.On !== this.accessory.context.On) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges` + + ` On: ${this.LightBulb.On}, OnCached: ${this.accessory.context.On}`); const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -607,11 +491,11 @@ export class ColorBulb { id: this.device.bleMac, }) .then(async (device_list: any) => { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); - return await this.retry({ - max: this.maxRetry(), + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); + return await this.retryBLE({ + max: await this.maxRetryBLE(), fn: async () => { - if (this.On) { + if (this.LightBulb.On) { return await device_list[0].turnOn({ id: this.device.bleMac }); } else { return await device_list[0].turnOff({ id: this.device.bleMac }); @@ -621,38 +505,37 @@ export class ColorBulb { }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.On = false; + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `On: ${this.LightBulb.On} sent over BLE, sent successfully`); + this.LightBulb.On = false; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); // Push Brightness Update - if (this.On) { + if (this.LightBulb.On) { await this.BLEpushBrightnessChanges(); } // Push ColorTemperature Update - if (this.On) { + if (this.LightBulb.On) { await this.BLEpushColorTemperatureChanges(); } // Push Hue & Saturation Update - if (this.On) { + if (this.LightBulb.On) { await this.BLEpushRGBChanges(); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges.` + `On: ${this.On}, ` + `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges, On: ${this.LightBulb.On},` + + ` OnCached: ${this.accessory.context.On}`); } } async BLEpushBrightnessChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushBrightnessChanges`); - if (this.Brightness !== this.accessory.context.Brightness) { + if (this.LightBulb.Brightness !== this.accessory.context.Brightness) { const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -666,33 +549,28 @@ export class ColorBulb { id: this.device.bleMac, }) .then(async (device_list: any) => { - this.infoLog(`${this.accessory.displayName} Target Brightness: ${this.Brightness}`); - return await device_list[0].setBrightness(this.Brightness); + this.infoLog(`${this.accessory.displayName} Target Brightness: ${this.LightBulb.Brightness}`); + return await device_list[0].setBrightness(this.LightBulb.Brightness); }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.On = false; + this.LightBulb.On = false; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushBrightnessChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushBrightnessChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushBrightnessChanges.` + - `Brightness: ${this.Brightness}, ` + - `BrightnessCached: ${this.accessory.context.Brightness}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushBrightnessChanges.` + + `Brightness: ${this.LightBulb.Brightness}, BrightnessCached: ${this.accessory.context.Brightness}`); } } async BLEpushColorTemperatureChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushColorTemperatureChanges`); - if (this.ColorTemperature !== this.accessory.context.ColorTemperature) { + if (this.LightBulb.ColorTemperature !== this.accessory.context.ColorTemperature) { const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -706,36 +584,32 @@ export class ColorBulb { id: this.device.bleMac, }) .then(async (device_list: any) => { - this.infoLog(`${this.accessory.displayName} Target ColorTemperature: ${this.ColorTemperature}`); - return await device_list[0].setColorTemperature(this.ColorTemperature); + this.infoLog(`${this.accessory.displayName} Target ColorTemperature: ${this.LightBulb.ColorTemperature}`); + return await device_list[0].setColorTemperature(this.LightBulb.ColorTemperature); }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.On = false; + this.LightBulb.On = false; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushColorTemperatureChanges with ` + - `${this.device.connectionType} Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushColorTemperatureChanges with ` + + `${this.device.connectionType} Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushColorTemperatureChanges.` + - `ColorTemperature: ${this.ColorTemperature}, ColorTemperatureCached: ${this.accessory.context.ColorTemperature}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushColorTemperatureChanges.` + + `ColorTemperature: ${this.LightBulb.ColorTemperature}, ColorTemperatureCached: ${this.accessory.context.ColorTemperature}`); } } async BLEpushRGBChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushRGBChanges`); - if (this.Hue !== this.accessory.context.Hue || this.Saturation !== this.accessory.context.Saturation) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${JSON.stringify(this.Hue)}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${JSON.stringify(this.Saturation)}`); + if (this.LightBulb.Hue !== this.accessory.context.Hue || this.LightBulb.Saturation !== this.accessory.context.Saturation) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${JSON.stringify(this.LightBulb.Hue)}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${JSON.stringify(this.LightBulb.Saturation)}`); - const [red, green, blue] = hs2rgb(Number(this.Hue), Number(this.Saturation)); + const [red, green, blue] = hs2rgb(Number(this.LightBulb.Hue), Number(this.LightBulb.Saturation)); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} rgb: ${JSON.stringify([red, green, blue])}`); const switchbot = await this.platform.connectBLE(); @@ -751,34 +625,30 @@ export class ColorBulb { id: this.device.bleMac, }) .then(async (device_list: any) => { - this.infoLog(`${this.accessory.displayName} Target RGB: ${(this.Brightness, red, green, blue)}`); - return await device_list[0].setRGB(this.Brightness, red, green, blue); + this.infoLog(`${this.accessory.displayName} Target RGB: ${(this.LightBulb.Brightness, red, green, blue)}`); + return await device_list[0].setRGB(this.LightBulb.Brightness, red, green, blue); }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.On = false; + this.LightBulb.On = false; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushRGBChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushRGBChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushRGBChanges. Hue: ${this.Hue}, ` + - `HueCached: ${this.accessory.context.Hue}, Saturation: ${this.Saturation}, SaturationCached: ${this.accessory.context.Saturation}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushRGBChanges. Hue: ${this.LightBulb.Hue}, HueCached: ` + + `${this.accessory.context.Hue}, Saturation: ${this.LightBulb.Saturation}, SaturationCached: ${this.accessory.context.Saturation}`); } } async openAPIpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`); - if (this.On !== this.accessory.context.On) { + if (this.LightBulb.On !== this.accessory.context.On) { let command = ''; - if (this.On) { + if (this.LightBulb.On) { command = 'turnOn'; } else { command = 'turnOff'; @@ -803,44 +673,41 @@ export class ColorBulb { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.` + - `On: ${this.On}, ` + - `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges,` + + `On: ${this.LightBulb.On}, OnCached: ${this.accessory.context.On}`); } // Push Hue & Saturation Update - if (this.On) { + if (this.LightBulb.On) { await this.pushHueSaturationChanges(); } // Push ColorTemperature Update - if (this.On) { + if (this.LightBulb.On) { await this.pushColorTemperatureChanges(); } // Push Brightness Update - if (this.On) { + if (this.LightBulb.On) { await this.pushBrightnessChanges(); } } async pushHueSaturationChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushHueSaturationChanges`); - if (this.Hue !== this.accessory.context.Hue || this.Saturation !== this.accessory.context.Saturation) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${JSON.stringify(this.Hue)}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${JSON.stringify(this.Saturation)}`); - const [red, green, blue] = hs2rgb(Number(this.Hue), Number(this.Saturation)); + if (this.LightBulb.Hue !== this.accessory.context.Hue || this.LightBulb.Saturation !== this.accessory.context.Saturation) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${JSON.stringify(this.LightBulb.Hue)}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${JSON.stringify(this.LightBulb.Saturation)}`); + const [red, green, blue] = hs2rgb(Number(this.LightBulb.Hue), Number(this.LightBulb.Saturation)); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} rgb: ${JSON.stringify([red, green, blue])}`); const bodyChange = JSON.stringify({ command: 'setColor', @@ -862,30 +729,28 @@ export class ColorBulb { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushHueSaturationChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushHueSaturationChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No pushHueSaturationChanges. Hue: ${this.Hue}, ` + - `HueCached: ${this.accessory.context.Hue}, Saturation: ${this.Saturation}, SaturationCached: ${this.accessory.context.Saturation}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushHueSaturationChanges. Hue: ${this.LightBulb.Hue}, HueCached: ` + + `${this.accessory.context.Hue}, Saturation: ${this.LightBulb.Saturation}, SaturationCached: ${this.accessory.context.Saturation}`); } } async pushColorTemperatureChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushColorTemperatureChanges`); - if (this.ColorTemperature !== this.accessory.context.ColorTemperature) { - const kelvin = Math.round(1000000 / Number(this.ColorTemperature)); - this.cacheKelvin = kelvin; + if (this.LightBulb.ColorTemperature !== this.accessory.context.ColorTemperature) { + const kelvin = Math.round(1000000 / Number(this.LightBulb.ColorTemperature)); + this.accessory.context.kelvin = kelvin; const bodyChange = JSON.stringify({ command: 'setColorTemperature', parameter: `${kelvin}`, @@ -906,31 +771,29 @@ export class ColorBulb { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushColorTemperatureChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushColorTemperatureChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No pushColorTemperatureChanges.` + - `ColorTemperature: ${this.ColorTemperature}, ColorTemperatureCached: ${this.accessory.context.ColorTemperature}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushColorTemperatureChanges.` + + `ColorTemperature: ${this.LightBulb.ColorTemperature}, ColorTemperatureCached: ${this.accessory.context.ColorTemperature}`); } } async pushBrightnessChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushBrightnessChanges`); - if (this.Brightness !== this.accessory.context.Brightness) { + if (this.LightBulb.Brightness !== this.accessory.context.Brightness) { const bodyChange = JSON.stringify({ command: 'setBrightness', - parameter: `${this.Brightness}`, + parameter: `${this.LightBulb.Brightness}`, commandType: 'command', }); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`); @@ -948,23 +811,20 @@ export class ColorBulb { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushBrightnessChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushBrightnessChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No pushBrightnessChanges.` + - `Brightness: ${this.Brightness}, ` + - `BrightnessCached: ${this.accessory.context.Brightness}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushBrightnessChanges.` + + `Brightness: ${this.LightBulb.Brightness}, BrightnessCached: ${this.accessory.context.Brightness}`); } } @@ -972,13 +832,13 @@ export class ColorBulb { * Handle requests to set the value of the "On" characteristic */ async OnSet(value: CharacteristicValue): Promise { - if (this.On === this.accessory.context.On) { + if (this.LightBulb.On === this.accessory.context.On) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set On: ${value}`); } else { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set On: ${value}`); } - this.On = value; + this.LightBulb.On = value; this.doColorBulbUpdate.next(); } @@ -986,15 +846,15 @@ export class ColorBulb { * Handle requests to set the value of the "Brightness" characteristic */ async BrightnessSet(value: CharacteristicValue): Promise { - if (this.Brightness === this.accessory.context.Brightness) { + if (this.LightBulb.Brightness === this.accessory.context.Brightness) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Brightness: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`); } - this.Brightness = value; + this.LightBulb.Brightness = value; this.doColorBulbUpdate.next(); } @@ -1002,30 +862,32 @@ export class ColorBulb { * Handle requests to set the value of the "ColorTemperature" characteristic */ async ColorTemperatureSet(value: CharacteristicValue): Promise { - if (this.ColorTemperature === this.accessory.context.ColorTemperature) { + if (this.LightBulb.ColorTemperature === this.accessory.context.ColorTemperature) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set ColorTemperature: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set ColorTemperature: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set ColorTemperature: ${value}`); } + const minKelvin = 2000; + const maxKelvin = 9000; // Convert mired to kelvin to nearest 100 (SwitchBot seems to need this) const kelvin = Math.round(1000000 / Number(value) / 100) * 100; // Check and increase/decrease kelvin to range of device - const k = Math.min(Math.max(kelvin, this.minKelvin), this.maxKelvin); + const k = Math.min(Math.max(kelvin, minKelvin), maxKelvin); - if (!this.accessory.context.On || this.cacheKelvin === k) { + if (!this.accessory.context.On || this.accessory.context.kelvin === k) { return; } // Updating the hue/sat to the corresponding values mimics native adaptive lighting const hs = m2hs(value); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Hue, hs[0]); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Saturation, hs[1]); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Hue, hs[0]); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Saturation, hs[1]); - this.ColorTemperature = value; + this.LightBulb.ColorTemperature = value; this.doColorBulbUpdate.next(); } @@ -1033,17 +895,17 @@ export class ColorBulb { * Handle requests to set the value of the "Hue" characteristic */ async HueSet(value: CharacteristicValue): Promise { - if (this.Hue === this.accessory.context.Hue) { + if (this.LightBulb.Hue === this.accessory.context.Hue) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Hue: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Hue: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Hue: ${value}`); } - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); - this.Hue = value; + this.LightBulb.Hue = value; this.doColorBulbUpdate.next(); } @@ -1051,60 +913,61 @@ export class ColorBulb { * Handle requests to set the value of the "Saturation" characteristic */ async SaturationSet(value: CharacteristicValue): Promise { - if (this.Saturation === this.accessory.context.Saturation) { + if (this.LightBulb.Saturation === this.accessory.context.Saturation) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Saturation: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Saturation: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Saturation: ${value}`); } - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); - this.Saturation = value; + this.LightBulb.Saturation = value; this.doColorBulbUpdate.next(); } async updateHomeKitCharacteristics(): Promise { // On - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.LightBulb.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); } else { - this.accessory.context.On = this.On; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + this.accessory.context.On = this.LightBulb.On; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.On, this.LightBulb.On); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.LightBulb.On}`); } // Brightness - if (this.Brightness === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.Brightness}`); + if (this.LightBulb.Brightness === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); } else { - this.accessory.context.Brightness = this.Brightness; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Brightness, this.Brightness); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Brightness: ${this.Brightness}`); + this.accessory.context.Brightness = this.LightBulb.Brightness; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Brightness, this.LightBulb.Brightness); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Brightness: ${this.LightBulb.Brightness}`); } // ColorTemperature - if (this.ColorTemperature === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.ColorTemperature}`); + if (this.LightBulb.ColorTemperature === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); } else { - this.accessory.context.ColorTemperature = this.ColorTemperature; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.ColorTemperature, this.ColorTemperature); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic ColorTemperature: ${this.ColorTemperature}`); + this.accessory.context.ColorTemperature = this.LightBulb.ColorTemperature; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, this.LightBulb.ColorTemperature); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` ColorTemperature: ${this.LightBulb.ColorTemperature}`); } // Hue - if (this.Hue === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.Hue}`); + if (this.LightBulb.Hue === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.LightBulb.Hue}`); } else { - this.accessory.context.Hue = this.Hue; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Hue, this.Hue); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Hue: ${this.Hue}`); + this.accessory.context.Hue = this.LightBulb.Hue; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Hue, this.LightBulb.Hue); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Hue: ${this.LightBulb.Hue}`); } // Saturation - if (this.Saturation === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.Saturation}`); + if (this.LightBulb.Saturation === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); } else { - this.accessory.context.Saturation = this.Saturation; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Saturation, this.Saturation); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Saturation: ${this.Saturation}`); + this.accessory.context.Saturation = this.LightBulb.Saturation; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Saturation, this.LightBulb.Saturation); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Saturation: ${this.LightBulb.Saturation}`); } } @@ -1118,35 +981,6 @@ export class ColorBulb { } } - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); - } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} customBLEaddress: ${this.device.customBLEaddress}`); - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'u', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); - } - } - async BLEPushConnection() { if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') { this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Push Changes`); @@ -1163,284 +997,17 @@ export class ColorBulb { } } - async retry({ max, fn }: { max: number; fn: { (): any; (): Promise } }): Promise { - return fn().catch(async (e: any) => { - if (max === 0) { - throw e; - } - this.infoLog(e); - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Retrying`); - await sleep(1000); - return this.retry({ max: max - 1, fn }); - }); - } - - maxRetry(): number { - if (this.device.maxRetry) { - return this.device.maxRetry; - } else { - return 5; - } - } - - minStep(device: device & devicesConfig): number { - if (device.colorbulb?.set_minStep) { - this.set_minStep = device.colorbulb?.set_minStep; - } else { - this.set_minStep = 1; - } - return this.set_minStep; - } - - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.On, false); } } apiError(e: any): void { - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.On, e); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Hue, e); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Brightness, e); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Saturation, e); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.ColorTemperature, e); - } - - async deviceContext(): Promise { - if (this.On === undefined) { - this.On = false; - } else { - this.On = this.accessory.context.On; - } - if (this.Hue === undefined) { - this.Hue = 0; - } else { - this.Hue = this.accessory.context.Hue; - } - if (this.Brightness === undefined) { - this.Brightness = 0; - } else { - this.Brightness = this.accessory.context.Brightness; - } - if (this.Saturation === undefined) { - this.Saturation = 0; - } else { - this.Saturation = this.accessory.context.Saturation; - } - if (this.ColorTemperature === undefined) { - this.ColorTemperature = 140; - } else { - this.ColorTemperature = this.accessory.context.ColorTemperature; - } - this.minKelvin = 2000; - this.maxKelvin = 9000; - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.colorbulb) { - config = device.colorbulb; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.offline !== undefined) { - config['offline'] = device.offline; - } - if (device.maxRetry !== undefined) { - config['maxRetry'] = device.maxRetry; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.On, e); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Hue, e); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Brightness, e); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Saturation, e); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, e); } } diff --git a/src/device/contact.ts b/src/device/contact.ts index 0c1fba58..43fc399b 100644 --- a/src/device/contact.ts +++ b/src/device/contact.ts @@ -1,324 +1,296 @@ -import { request } from 'undici'; -import { sleep } from '../utils.js'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * contact.ts: @switchbot/homebridge-switchbot. + */ +import { deviceBase } from './device.js'; import { interval, Subject } from 'rxjs'; +import { Devices } from '../settings.js'; import { skipWhile } from 'rxjs/operators'; -import { SwitchBotPlatform } from '../platform.js'; -import { Service, PlatformAccessory, CharacteristicValue, API, Logging, HAP } from 'homebridge'; -import { device, devicesConfig, serviceData, deviceStatus, Devices, SwitchBotPlatformConfig } from '../settings.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class Contact { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class Contact extends deviceBase { // Services - motionService?: Service; - batteryService: Service; - lightSensorService?: Service; - contactSensorservice: Service; - - // Characteristic Values - BatteryLevel!: CharacteristicValue; - MotionDetected!: CharacteristicValue; - StatusLowBattery!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - ContactSensorState!: CharacteristicValue; - CurrentAmbientLightLevel!: CharacteristicValue; - - // OpenAPI Status - OpenAPI_BatteryLevel: deviceStatus['battery']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_MotionDetected: deviceStatus['moveDetected']; - OpenAPI_ContactSensorState: deviceStatus['openState']; - OpenAPI_CurrentAmbientLightLevel: deviceStatus['brightness']; - - // BLE Status - BLE_BatteryLevel!: serviceData['battery']; - BLE_MotionDetected!: serviceData['movement']; - BLE_ContactSensorState!: serviceData['doorState']; - BLE_CurrentAmbientLightLevel: serviceData['lightLevel']; - - // BLE Others - scanning!: boolean; - BLE_IsConnected?: boolean; - - // Config - set_minLux!: number; - set_maxLux!: number; - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; + private ContactSensor: { + Name: CharacteristicValue; + Service: Service; + ContactSensorState: CharacteristicValue; + }; + + private Battery: { + Name: CharacteristicValue; + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + }; + + private MotionSensor?: { + Name: CharacteristicValue; + Service: Service; + MotionDetected: CharacteristicValue; + }; + + private LightSensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentAmbientLightLevel: CharacteristicValue; + }; // Updates - contactUbpdateInProgress!: boolean; + contactUpdateInProgress!: boolean; doContactUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); - this.deviceContext(); - this.deviceConfig(device); - + super(platform, accessory, device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doContactUpdate = new Subject(); - this.contactUbpdateInProgress = false; - - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, 'W1201500') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Contact service if it exists, otherwise create a new Contact service - // you can create multiple services for each accessory - const contactSensorservice = `${accessory.displayName} Contact Sensor`; - (this.contactSensorservice = accessory.getService(this.hap.Service.ContactSensor) - || accessory.addService(this.hap.Service.ContactSensor)), contactSensorservice; + this.contactUpdateInProgress = false; + + // Initialize Contact Sensor Service + accessory.context.ContactSensor = accessory.context.ContactSensor ?? {}; + this.ContactSensor = { + Name: accessory.context.ContactSensor.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.ContactSensor) ?? accessory.addService(this.hap.Service.ContactSensor) as Service, + ContactSensorState: accessory.context.ContactSensorState ?? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED, + }; + accessory.context.ContactSensor = this.ContactSensor as object; + + // Initialize ContactSensor Characteristics + this.ContactSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.ContactSensor.Name) + .setCharacteristic(this.hap.Characteristic.StatusActive, true) + .getCharacteristic(this.hap.Characteristic.ContactSensorState) + .onGet(() => { + return this.ContactSensor.ContactSensorState; + }); - this.contactSensorservice.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.contactSensorservice.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.contactSensorservice.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } + // Initialize Battery Service + accessory.context.Battery = accessory.context.Battery ?? {}; + this.Battery = { + Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Characteristics + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE) + .getCharacteristic(this.hap.Characteristic.BatteryLevel) + .onGet(() => { + return this.Battery.BatteryLevel; + }); - // Motion Sensor Service - if (device.contact?.hide_motionsensor) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Motion Sensor Service`); - this.motionService = this.accessory.getService(this.hap.Service.MotionSensor); - accessory.removeService(this.motionService!); - } else if (!this.motionService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Motion Sensor Service`); - const motionService = `${accessory.displayName} Motion Sensor`; - (this.motionService = this.accessory.getService(this.hap.Service.MotionSensor) - || this.accessory.addService(this.hap.Service.MotionSensor)), motionService; + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery) + .getCharacteristic(this.hap.Characteristic.StatusLowBattery) + .onGet(() => { + return this.Battery.StatusLowBattery; + }); - this.motionService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Motion Sensor`); - this.motionService.setCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Motion Sensor`); + // Initialize Motion Sensor Service + if (this.device.contact?.hide_motionsensor) { + if (this.MotionSensor) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} Removing Motion Sensor Service`); + this.MotionSensor.Service = accessory.getService(this.hap.Service.MotionSensor) as Service; + accessory.removeService(this.MotionSensor.Service); + } } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Motion Sensor Service Not Added`); + accessory.context.MotionSensor = accessory.context.MotionSensor ?? {}; + this.MotionSensor = { + Name: accessory.context.MotionSensor.Name ?? `${accessory.displayName} Motion Sensor`, + Service: accessory.getService(this.hap.Service.MotionSensor) ?? accessory.addService(this.hap.Service.MotionSensor) as Service, + MotionDetected: accessory.context.MotionDetected ?? false, + }; + accessory.context.MotionSensor = this.MotionSensor as object; + + // Motion Sensor Characteristics + this.MotionSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.MotionSensor.Name) + .setCharacteristic(this.hap.Characteristic.StatusActive, true) + .getCharacteristic(this.hap.Characteristic.MotionDetected) + .onGet(() => { + return this.MotionSensor!.MotionDetected; + }); } - // Light Sensor Service + // Initialize Light Sensor Service if (device.contact?.hide_lightsensor) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Light Sensor Service`); - this.lightSensorService = this.accessory.getService(this.hap.Service.LightSensor); - accessory.removeService(this.lightSensorService!); - } else if (!this.lightSensorService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Light Sensor Service`); - - const lightSensorService = `${accessory.displayName} Light Sensor`; - (this.lightSensorService = this.accessory.getService(this.hap.Service.LightSensor) - || this.accessory.addService(this.hap.Service.LightSensor)), lightSensorService; - - this.lightSensorService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Light Sensor`); - this.lightSensorService.setCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Light Sensor`); + if (this.LightSensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Light Sensor Service`); + this.LightSensor.Service = accessory.getService(this.hap.Service.LightSensor) as Service; + accessory.removeService(this.LightSensor.Service); + } } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Light Sensor Service Not Added`); + accessory.context.LightSensor = accessory.context.LightSensor ?? {}; + this.LightSensor = { + Name: accessory.context.LightSensor.Name ?? `${accessory.displayName} Light Sensor`, + Service: accessory.getService(this.hap.Service.LightSensor) ?? accessory.addService(this.hap.Service.LightSensor) as Service, + CurrentAmbientLightLevel: accessory.context.CurrentAmbientLightLevel ?? 0.0001, + }; + accessory.context.LightSensor = this.LightSensor as object; + + // Light Sensor Characteristics + this.LightSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LightSensor.Name) + .setCharacteristic(this.hap.Characteristic.StatusActive, true) + .getCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel) + .onGet(() => { + return this.LightSensor!.CurrentAmbientLightLevel; + }); } - // Battery Service - const batteryService = `${accessory.displayName} Battery`; - (this.batteryService = this.accessory.getService(this.hap.Service.Battery) - || accessory.addService(this.hap.Service.Battery)), batteryService; - - this.batteryService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Battery`); - if (!this.batteryService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.batteryService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Battery`); - } - this.batteryService.setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE); + // Retrieve initial values and updateHomekit + this.refreshStatus(); // Retrieve initial values and updateHomekit this.updateHomeKitCharacteristics(); // Start an update interval interval(this.deviceRefreshRate * 1000) - .pipe(skipWhile(() => this.contactUbpdateInProgress)) + .pipe(skipWhile(() => this.contactUpdateInProgress)) .subscribe(async () => { await this.refreshStatus(); }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - const { detectionState, brightness, openState } = context; - const { MotionDetected, CurrentAmbientLightLevel, ContactSensorState } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(detectionState, brightness, openState) = ' + - `Webhook:(${detectionState}, ${brightness}, ${openState}), ` + - `current:(${MotionDetected}, ${CurrentAmbientLightLevel}, ${ContactSensorState})`); - this.set_minLux = this.minLux(); - this.set_maxLux = this.maxLux(); - this.MotionDetected = detectionState === 'DETECTED' ? true : false; - this.CurrentAmbientLightLevel = brightness === 'bright' ? this.set_maxLux : this.set_minLux; - this.ContactSensorState = openState === 'open' ? 1 : 0; - this.updateHomeKitCharacteristics(); - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); } - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // Door State - switch (this.BLE_ContactSensorState) { + switch (serviceData.doorState) { case 'open': case 1: - this.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; + this.ContactSensor.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; break; case 'close': case 0: - this.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED; + this.ContactSensor.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED; break; default: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} timeout no closed, doorstate: ${this.BLE_ContactSensorState}`); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} timeout no closed, doorstate: ${serviceData.doorState}`); } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ContactSensorState: ${this.ContactSensorState}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ContactSensorState: ${this.ContactSensor.ContactSensorState}`); if ( - this.ContactSensorState !== this.accessory.context.ContactSensorState && + this.ContactSensor.ContactSensorState !== this.accessory.context.ContactSensorState && this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED ) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Opened`); } // Movement if (!this.device.contact?.hide_motionsensor) { - this.MotionDetected = Boolean(this.BLE_MotionDetected); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionDetected}`); - if (this.MotionDetected !== this.accessory.context.MotionDetected && this.MotionDetected) { + this.MotionSensor!.MotionDetected = Boolean(serviceData.movement); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionSensor!.MotionDetected}`); + if (this.MotionSensor!.MotionDetected !== this.accessory.context.MotionDetected && this.MotionSensor!.MotionDetected) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Detected Motion`); } } // Light Level if (!this.device.contact?.hide_lightsensor) { - this.set_minLux = this.minLux(); - this.set_maxLux = this.maxLux(); - switch (this.BLE_CurrentAmbientLightLevel) { + const set_minLux = this.device.contact?.set_minLux ?? 1; + const set_maxLux = this.device.contact?.set_maxLux ?? 6001; + switch (serviceData.lightLevel) { case true: - this.CurrentAmbientLightLevel = this.set_minLux; + this.LightSensor!.CurrentAmbientLightLevel = set_minLux; break; default: - this.CurrentAmbientLightLevel = this.set_maxLux; + this.LightSensor!.CurrentAmbientLightLevel = set_maxLux; } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel},` + - ` CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`, - ); - if (this.CurrentAmbientLightLevel !== this.accessory.context.CurrentAmbientLightLevel) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel},` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); + if (this.LightSensor!.CurrentAmbientLightLevel !== this.accessory.context.CurrentAmbientLightLevel) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } } // Battery - if (this.BLE_BatteryLevel === undefined) { - this.BLE_BatteryLevel === 100; + if (serviceData.battery === undefined) { + serviceData.battery === 100; } - this.BatteryLevel = Number(this.BLE_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(serviceData.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}, ` + `StatusLowBattery: ${this.StatusLowBattery}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}, ` + + `StatusLowBattery: ${this.Battery.StatusLowBattery}`); } - async openAPIparseStatus(): Promise { + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); // Contact State - if (this.OpenAPI_ContactSensorState === 'open') { - this.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ContactSensorState: ${this.ContactSensorState}`); - } else if (this.OpenAPI_ContactSensorState === 'close') { - this.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ContactSensorState: ${this.ContactSensorState}`); + if (deviceStatus.body.openState === 'open') { + this.ContactSensor.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ContactSensorState: ${this.ContactSensor.ContactSensorState}`); + } else if (deviceStatus.body.openState === 'close') { + this.ContactSensor.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ContactSensorState: ${this.ContactSensor.ContactSensorState}`); } else { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openState: ${this.OpenAPI_ContactSensorState}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openState: ${deviceStatus.body.openState}`); } // Motion State if (!this.device.contact?.hide_motionsensor) { - this.MotionDetected = this.OpenAPI_MotionDetected!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionDetected}`); + this.MotionSensor!.MotionDetected = deviceStatus.body.moveDetected!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionSensor!.MotionDetected}`); } // Light Level if (!this.device.contact?.hide_lightsensor) { - this.set_minLux = this.minLux(); - this.set_maxLux = this.maxLux(); - switch (this.OpenAPI_CurrentAmbientLightLevel) { + const set_minLux = this.device.contact?.set_minLux ?? 1; + const set_maxLux = this.device.contact?.set_maxLux ?? 6001; + switch (deviceStatus.body.brightness) { case 'dim': - this.CurrentAmbientLightLevel = this.set_minLux; + this.LightSensor!.CurrentAmbientLightLevel = set_minLux; break; case 'bright': default: - this.CurrentAmbientLightLevel = this.set_maxLux; + this.LightSensor!.CurrentAmbientLightLevel = set_maxLux; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } // Battery - if (this.OpenAPI_BatteryLevel === undefined) { - this.OpenAPI_BatteryLevel === 100; + if (deviceStatus.body.battery === undefined) { + deviceStatus.body.battery === 100; } - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}, ` + + `StatusLowBattery: ${this.Battery.StatusLowBattery}`); + + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}, ` + `StatusLowBattery: ${this.StatusLowBattery}`, - ); - - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; } /** @@ -333,10 +305,8 @@ export class Contact { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -344,126 +314,95 @@ export class Contact { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLERefreshStatus`); const switchbot = await this.platform.connectBLE(); // Convert to BLE Address - this.device.bleMac = - this.device.customBLEaddress || - this.device - .deviceId!.match(/.{1,2}/g)! - .join(':') - .toLowerCase(); + this.device.bleMac = this.device + .deviceId!.match(/.{1,2}/g)! + .join(':') + .toLowerCase(); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE Address: ${this.device.bleMac}`); this.getCustomBLEAddress(switchbot); // Start to monitor advertisement packets (async () => { - await switchbot.startScan({ - model: 'd', - id: this.device.bleMac, - }); + // Start to monitor advertisement packets + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === 'd') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_MotionDetected = ad.serviceData.movement; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.BLE_CurrentAmbientLightLevel = ad.serviceData.lightLevel; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - await switchbot - .startScan({ - model: 'd', - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - this.scanning = true; - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.BLE_MotionDetected = ad.serviceData.movement; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.BLE_ContactSensorState = ad.serviceData.doorState; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} movement: ${ad.serviceData.movement}, doorState: ` + - `${ad.serviceData.doorState}, battery: ${ad.serviceData.battery}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - this.scanning = false; - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - if (this.scanning) { - await this.stopScanning(switchbot); - } - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_ContactSensorState = deviceStatus.body.openState; - this.OpenAPI_MotionDetected = deviceStatus.body.moveDetected; - this.OpenAPI_CurrentAmbientLightLevel = deviceStatus.body.brightness; - this.OpenAPI_BatteryLevel = deviceStatus.body.battery; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); + if (deviceStatus.statusCode === 161 || deviceStatus.statusCode === 171) { + this.offlineOff(); + } } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { detectionState, brightness, openState } = context; + const { ContactSensorState } = this.ContactSensor; + const { CurrentAmbientLightLevel } = this.LightSensor ?? {}; + const { MotionDetected } = this.MotionSensor ?? {}; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (detectionState, brightness, openState) = Webhook:(${detectionState}, ` + + `${brightness}, ${openState}), current:(${MotionDetected}, ${CurrentAmbientLightLevel}, ${ContactSensorState})`); + const set_minLux = this.device.contact?.set_minLux ?? 1; + const set_maxLux = this.device.contact?.set_maxLux ?? 6001; + this.ContactSensor.ContactSensorState = openState === 'open' ? 1 : 0; + if (!device.contact?.hide_motionsensor) { + this.MotionSensor!.MotionDetected = detectionState === 'DETECTED' ? true : false; + } + if (!device.contact?.hide_lightsensor) { + this.LightSensor!.CurrentAmbientLightLevel = brightness === 'bright' ? set_maxLux : set_minLux; + } + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -471,75 +410,49 @@ export class Contact { * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics(): Promise { - if (this.ContactSensorState === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ContactSensorState: ${this.ContactSensorState}`); + if (this.ContactSensor.ContactSensorState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ContactSensorState: ${this.ContactSensor.ContactSensorState}`); } else { - this.accessory.context.ContactSensorState = this.ContactSensorState; - this.contactSensorservice.updateCharacteristic(this.hap.Characteristic.ContactSensorState, this.ContactSensorState); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic ContactSensorState: ${this.ContactSensorState}`); + this.accessory.context.ContactSensorState = this.ContactSensor.ContactSensorState; + this.ContactSensor.Service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, this.ContactSensor.ContactSensorState); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` ContactSensorState: ${this.ContactSensor.ContactSensorState}`); } if (!this.device.contact?.hide_motionsensor) { - if (this.MotionDetected === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionDetected}`); + if (this.MotionSensor!.MotionDetected === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionSensor!.MotionDetected}`); } else { - this.accessory.context.MotionDetected = this.MotionDetected; - this.motionService?.updateCharacteristic(this.hap.Characteristic.MotionDetected, this.MotionDetected); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic MotionDetected: ${this.MotionDetected}`); + this.accessory.context.MotionDetected = this.MotionSensor!.MotionDetected; + this.MotionSensor!.Service.updateCharacteristic(this.hap.Characteristic.MotionDetected, this.MotionSensor!.MotionDetected); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` MotionDetected: ${this.MotionSensor!.MotionDetected}`); } } if (!this.device.contact?.hide_lightsensor) { - if (this.CurrentAmbientLightLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + if (this.LightSensor?.CurrentAmbientLightLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentAmbientLightLevel: ${this.LightSensor?.CurrentAmbientLightLevel}`); } else { - this.accessory.context.CurrentAmbientLightLevel = this.CurrentAmbientLightLevel; - this.lightSensorService?.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.CurrentAmbientLightLevel); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` updateCharacteristic CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`, - ); + this.accessory.context.CurrentAmbientLightLevel = this.LightSensor.CurrentAmbientLightLevel; + this.LightSensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.LightSensor.CurrentAmbientLightLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` updateCharacteristic CurrentAmbientLightLevel: ${this.LightSensor.CurrentAmbientLightLevel}`); } } - if (this.BatteryLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}`); - } else { - this.accessory.context.BatteryLevel = this.BatteryLevel; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.BatteryLevel); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); - } - if (this.StatusLowBattery === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); } else { - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); } - } - - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'd', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); } } @@ -552,254 +465,31 @@ export class Contact { } } - minLux(): number { - if (this.device.contact?.set_minLux) { - this.set_minLux = this.device.contact!.set_minLux!; - } else { - this.set_minLux = 1; - } - return this.set_minLux; - } - - maxLux(): number { - if (this.device.contact?.set_maxLux) { - this.set_maxLux = this.device.contact!.set_maxLux!; - } else { - this.set_maxLux = 6001; - } - return this.set_maxLux; - } - - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); + this.ContactSensor.Service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, + this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED); + if (!this.device.contact?.hide_motionsensor) { + this.MotionSensor!.Service.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); + } + if (!this.device.contact?.hide_lightsensor) { + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, 100); + } } } async apiError(e: any): Promise { - this.contactSensorservice.updateCharacteristic(this.hap.Characteristic.ContactSensorState, e); + this.ContactSensor.Service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, e); + this.ContactSensor.Service.updateCharacteristic(this.hap.Characteristic.StatusActive, e); if (!this.device.contact?.hide_motionsensor) { - this.motionService?.updateCharacteristic(this.hap.Characteristic.MotionDetected, e); + this.MotionSensor!.Service.updateCharacteristic(this.hap.Characteristic.MotionDetected, e); + this.MotionSensor!.Service.updateCharacteristic(this.hap.Characteristic.StatusActive, e); } if (!this.device.contact?.hide_lightsensor) { - this.lightSensorService?.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, e); - } - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); - } - - async deviceContext() { - if (this.MotionDetected === undefined) { - this.MotionDetected = false; - } else { - this.MotionDetected = this.accessory.context.MotionDetected; - } - if (this.ContactSensorState === undefined) { - this.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED; - } else { - this.ContactSensorState = this.accessory.context.ContactSensorState; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.contact) { - config = device.contact; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, e); + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.StatusActive, e); } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); } } diff --git a/src/device/curtain.ts b/src/device/curtain.ts index 94c04fe6..56ae75c9 100644 --- a/src/device/curtain.ts +++ b/src/device/curtain.ts @@ -1,193 +1,181 @@ +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * curtain.ts: @switchbot/homebridge-switchbot. + */ +import { hostname } from 'os'; import { request } from 'undici'; -import { sleep } from '../utils.js'; -import { MqttClient } from 'mqtt'; import { interval, Subject } from 'rxjs'; -import asyncmqtt from 'async-mqtt'; -import { SwitchBotPlatform } from '../platform.js'; +import { deviceBase } from './device.js'; +import { Devices } from '../settings.js'; import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { Service, PlatformAccessory, CharacteristicValue, CharacteristicChange, API, Logging, HAP } from 'homebridge'; -import { device, devicesConfig, serviceData, deviceStatus, Devices, SwitchBotPlatformConfig } from '../settings.js'; -import { hostname } from 'os'; - -export class Curtain { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; - // Services - batteryService: Service; - lightSensorService?: Service; - windowCoveringService!: Service; - // Characteristic Values - BatteryLevel!: CharacteristicValue; - HoldPosition!: CharacteristicValue; - PositionState!: CharacteristicValue; - TargetPosition!: CharacteristicValue; - CurrentPosition!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - StatusLowBattery!: CharacteristicValue; - CurrentAmbientLightLevel?: CharacteristicValue; +import type { SwitchBotPlatform } from '../platform.js'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; +import type { Service, PlatformAccessory, CharacteristicValue, CharacteristicChange } from 'homebridge'; - // OpenAPI Status - OpenAPI_InMotion: deviceStatus['moving']; - OpenAPI_BatteryLevel: deviceStatus['battery']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_CurrentPosition: deviceStatus['slidePosition']; - OpenAPI_CurrentAmbientLightLevel: deviceStatus['brightness']; - - // Others - Mode!: string; - setPositionMode?: string; - - // BLE Status - BLE_InMotion: serviceData['inMotion']; - BLE_BatteryLevel: serviceData['battery']; - BLE_Calibration: serviceData['calibration']; - BLE_CurrentPosition: serviceData['position']; - BLE_CurrentAmbientLightLevel: serviceData['lightLevel']; - - // BLE Others - BLE_IsConnected?: boolean; - spaceBetweenLevels!: number; +export class Curtain extends deviceBase { + // Services + private WindowCovering: { + Name: CharacteristicValue; + Service: Service; + PositionState: CharacteristicValue; + TargetPosition: CharacteristicValue; + CurrentPosition: CharacteristicValue; + HoldPosition: CharacteristicValue; + }; + + private Battery: { + Name: CharacteristicValue; + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + ChargingState: CharacteristicValue; + }; + + private LightSensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentAmbientLightLevel?: CharacteristicValue; + }; // Target setNewTarget!: boolean; setNewTargetTimer!: NodeJS.Timeout; - //MQTT stuff - mqttClient: MqttClient | null = null; - - // Config - updateRate!: number; - set_minLux!: number; - set_maxLux!: number; - set_minStep!: number; - setOpenMode!: string; - setCloseMode!: string; - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; - // Updates curtainUpdateInProgress!: boolean; doCurtainUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - - // EVE history service handler - historyService: any = null; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.refreshRate(device); - this.scan(device); - this.setupMqtt(device); - this.deviceContext(); - this.deviceConfig(device); - + super(platform, accessory, device); + // default placeholder + this.history(device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doCurtainUpdate = new Subject(); this.curtainUpdateInProgress = false; this.setNewTarget = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, 'W0701600') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the WindowCovering service if it exists, otherwise create a new WindowCovering service - // you can create multiple services for each accessory - const windowCoveringService = `${accessory.displayName} ${device.deviceType}`; - (this.windowCoveringService = accessory.getService(this.hap.Service.WindowCovering) - || accessory.addService(this.hap.Service.WindowCovering)), windowCoveringService; - - this.windowCoveringService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.windowCoveringService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.windowCoveringService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/WindowCovering - - // create handlers for required characteristics - this.windowCoveringService.setCharacteristic(this.hap.Characteristic.PositionState, this.PositionState); + // Initialize WindowCovering Service + accessory.context.WindowCovering = accessory.context.WindowCovering ?? {}; + this.WindowCovering = { + Name: accessory.context.WindowCovering.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.WindowCovering) ?? accessory.addService(this.hap.Service.WindowCovering) as Service, + PositionState: accessory.context.PositionState ?? this.hap.Characteristic.PositionState.STOPPED, + TargetPosition: accessory.context.TargetPosition ?? 100, + CurrentPosition: accessory.context.CurrentPosition ?? 100, + HoldPosition: accessory.context.HoldPosition ?? false, + }; + accessory.context.WindowCovering = this.WindowCovering as object; + + // Initialize WindowCovering Service + this.WindowCovering.Service. + setCharacteristic(this.hap.Characteristic.Name, this.WindowCovering.Name) + .setCharacteristic(this.hap.Characteristic.ObstructionDetected, false) + .getCharacteristic(this.hap.Characteristic.PositionState) + .onGet(() => { + return this.WindowCovering.PositionState; + }); - this.windowCoveringService + // Initialize WindowCovering CurrentPosition + this.WindowCovering.Service .getCharacteristic(this.hap.Characteristic.CurrentPosition) .setProps({ - minStep: this.minStep(device), + minStep: device.curtain?.set_minStep ?? 1, minValue: 0, maxValue: 100, validValueRanges: [0, 100], }) .onGet(() => { - return this.CurrentPosition; + return this.WindowCovering.CurrentPosition; }); - this.windowCoveringService + // Initialize WindowCovering TargetPosition + this.WindowCovering.Service .getCharacteristic(this.hap.Characteristic.TargetPosition) .setProps({ - minStep: this.minStep(device), + minStep: device.curtain?.set_minStep ?? 1, minValue: 0, maxValue: 100, validValueRanges: [0, 100], }) + .onGet(() => { + return this.WindowCovering.TargetPosition; + }) .onSet(this.TargetPositionSet.bind(this)); - this.windowCoveringService + // Initialize WindowCovering TargetPosition + this.WindowCovering.Service .getCharacteristic(this.hap.Characteristic.HoldPosition) + .onGet(() => { + return this.WindowCovering.HoldPosition; + }) .onSet(this.HoldPositionSet.bind(this)); - // Light Sensor Service - if (device.curtain?.hide_lightsensor) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Light Sensor Service`); - this.lightSensorService = this.accessory.getService(this.hap.Service.LightSensor); - accessory.removeService(this.lightSensorService!); - } else if (!this.lightSensorService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Light Sensor Service`); - const lightSensorService = `${accessory.displayName} Light Sensor`; - (this.lightSensorService = this.accessory.getService(this.hap.Service.LightSensor) - || this.accessory.addService(this.hap.Service.LightSensor)), lightSensorService; + // Initialize Battery Service + accessory.context.Battery = accessory.context.Battery ?? {}; + this.Battery = { + Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + ChargingState: accessory.context.ChargingState ?? this.hap.Characteristic.ChargingState.NOT_CHARGING, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Service + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .getCharacteristic(this.hap.Characteristic.BatteryLevel) + .onGet(() => { + return this.Battery.BatteryLevel; + }); - this.lightSensorService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Light Sensor`); - if (!this.lightSensorService?.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.lightSensorService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Light Sensor`); - } - } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Light Sensor Service Not Added`); - } + this.Battery.Service + .getCharacteristic(this.hap.Characteristic.StatusLowBattery) + .onGet(() => { + return this.Battery.StatusLowBattery; + }); - // Battery Service - const batteryService = `${accessory.displayName} Battery`; - (this.batteryService = this.accessory.getService(this.hap.Service.Battery) - || accessory.addService(this.hap.Service.Battery)), batteryService; + this.Battery.Service + .getCharacteristic(this.hap.Characteristic.ChargingState) + .onGet(() => { + return this.Battery.ChargingState; + }); - this.batteryService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Battery`); - if (!this.batteryService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.batteryService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Battery`); + // Initialize LightSensor Service + if (device.curtain?.hide_lightsensor) { + if (this.LightSensor?.Service) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Light Sensor Service`); + this.LightSensor.Service = this.accessory.getService(this.hap.Service.LightSensor) as Service; + accessory.removeService(this.LightSensor.Service); + accessory.context.LightSensor = {}; + } + } else { + accessory.context.LightSensor = accessory.context.LightSensor ?? {}; + this.LightSensor = { + Name: accessory.context.LightSensor.Name ?? `${accessory.displayName} Light Sensor`, + Service: accessory.getService(this.hap.Service.LightSensor) ?? this.accessory.addService(this.hap.Service.LightSensor) as Service, + CurrentAmbientLightLevel: accessory.context.CurrentAmbientLightLevel ?? 0.0001, + }; + accessory.context.LightSensor = this.LightSensor as object; + + // Initialize LightSensor Characteristic + this.LightSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LightSensor.Name) + .setCharacteristic(this.hap.Characteristic.StatusActive, true) + .getCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel) + .onGet(() => { + return this.LightSensor!.CurrentAmbientLightLevel!; + }); } + // Retrieve initial values and updateHomekit + this.refreshStatus(); + // Update Homekit this.updateHomeKitCharacteristics(); @@ -199,35 +187,17 @@ export class Curtain { }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - const { slidePosition, battery } = context; - const { CurrentPosition, BatteryLevel } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(slidePosition, battery) = ' + - `Webhook:(${slidePosition}, ${battery}), ` + - `current:(${CurrentPosition}, ${BatteryLevel})`); - this.CurrentPosition = slidePosition; - this.BatteryLevel = battery; - this.updateHomeKitCharacteristics(); - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); // update slide progress - interval(this.updateRate * 1000) + interval(this.deviceUpdateRate * 1000) //.pipe(skipWhile(() => this.curtainUpdateInProgress)) .subscribe(async () => { - if (this.PositionState === this.hap.Characteristic.PositionState.STOPPED) { + if (this.WindowCovering.PositionState === this.hap.Characteristic.PositionState.STOPPED) { return; } - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Refresh Status When Moving, PositionState: ${this.PositionState}`); + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Refresh Status When Moving,` + + ` PositionState: ${this.WindowCovering.PositionState}`); await this.refreshStatus(); }); @@ -238,29 +208,37 @@ export class Curtain { tap(() => { this.curtainUpdateInProgress = true; }), - debounceTime(this.platform.config.options!.pushRate! * 1000), + debounceTime(this.devicePushRate * 1000), ) .subscribe(async () => { try { await this.pushChanges(); } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + - ` Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + + ` Error Message: ${JSON.stringify(e.message)}`); } this.curtainUpdateInProgress = false; }); // Setup EVE history features - this.setupHistoryService(device); + this.setupHistoryService(accessory, device); + } + + private history(device: device & devicesConfig) { + if (device.history === true) { + // initialize when this accessory is newly created. + this.accessory.context.lastActivation = this.accessory.context.lastActivation ?? 0; + } else { + // removes cached values if history is turned off + delete this.accessory.context.lastActivation; + } } /* * Setup EVE history features for curtain devices. */ - async setupHistoryService(device: device & devicesConfig): Promise { + async setupHistoryService(accessory: PlatformAccessory, device: device & devicesConfig): Promise { if (device.history !== true) { return; } @@ -295,8 +273,7 @@ export class Curtain { this.accessory.context.lastActivation = entry.time; sensor?.updateCharacteristic( this.platform.eve.Characteristics.LastActivation, - Math.max(0, this.accessory.context.lastActivation - this.historyService.getInitialTime()), - ); + Math.max(0, this.accessory.context.lastActivation - this.historyService.getInitialTime())); this.historyService.addEntry(entry); } }); @@ -304,7 +281,7 @@ export class Curtain { } async updateHistory(): Promise { - const motion = Number(this.CurrentPosition) > 0 ? 1 : 0; + const motion = Number(this.WindowCovering.CurrentPosition) > 0 ? 1 : 0; this.historyService.addEntry({ time: Math.round(new Date().valueOf() / 1000), motion: motion, @@ -317,198 +294,200 @@ export class Curtain { ); } - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // CurrentPosition - this.CurrentPosition = 100 - Number(this.BLE_CurrentPosition); + this.WindowCovering.CurrentPosition = 100 - Number(serviceData.position); await this.setMinMax(); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition ${this.CurrentPosition}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition ${this.WindowCovering.CurrentPosition}`); if (this.setNewTarget) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Checking Status ...`); await this.setMinMax(); - if (Number(this.TargetPosition) > this.CurrentPosition) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Closing, CurrentPosition: ${this.CurrentPosition}`); - this.PositionState = this.hap.Characteristic.PositionState.INCREASING; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} INCREASING PositionState: ${this.PositionState}`); - } else if (Number(this.TargetPosition) < this.CurrentPosition) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Opening, CurrentPosition: ${this.CurrentPosition}`); - this.PositionState = this.hap.Characteristic.PositionState.DECREASING; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} DECREASING PositionState: ${this.PositionState}`); + if (Number(this.WindowCovering.TargetPosition) > this.WindowCovering.CurrentPosition) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Closing, CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.INCREASING; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} INCREASING` + + ` PositionState: ${this.WindowCovering.PositionState}`); + } else if (Number(this.WindowCovering.TargetPosition) < this.WindowCovering.CurrentPosition) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Opening, CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.DECREASING; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} DECREASING` + + ` PositionState: ${this.WindowCovering.PositionState}`); } else { - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} Standby, CurrentPosition: ${this.CurrentPosition}`); - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} STOPPED PositionState: ${this.PositionState}`); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} Standby,` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.STOPPED; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} STOPPED` + + ` PositionState: ${this.WindowCovering.PositionState}`); } } else { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Standby, CurrentPosition: ${this.CurrentPosition}`); - this.TargetPosition = this.CurrentPosition; - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Standby, CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.TargetPosition = this.WindowCovering.CurrentPosition; + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.STOPPED; this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Stopped`); } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.CurrentPosition},` + - ` TargetPosition: ${this.TargetPosition}, PositionState: ${this.PositionState},`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.WindowCovering.CurrentPosition},` + + ` TargetPosition: ${this.WindowCovering.TargetPosition}, PositionState: ${this.WindowCovering.PositionState},`); if (!this.device.curtain?.hide_lightsensor) { - this.set_minLux = this.minLux(); - this.set_maxLux = this.maxLux(); - this.spaceBetweenLevels = 9; + const set_minLux = this.device.curtain?.set_minLux ?? 1; + const set_maxLux = this.device.curtain?.set_maxLux ?? 6001; + const spaceBetweenLevels = 9; // Brightness - switch (this.BLE_CurrentAmbientLightLevel) { + switch (serviceData.lightLevel) { case 1: - this.CurrentAmbientLightLevel = this.set_minLux; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = set_minLux; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); break; case 2: - this.CurrentAmbientLightLevel = (this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels; + this.LightSensor!.CurrentAmbientLightLevel = (set_maxLux - set_minLux) / spaceBetweenLevels; this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel},` + - ` Calculation: ${(this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels}`, - ); + `${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel},` + + ` Calculation: ${(set_maxLux - set_minLux) / spaceBetweenLevels}`); break; case 3: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 2; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 2; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); + this.Battery.ChargingState = this.hap.Characteristic.ChargingState.CHARGING; break; case 4: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 3; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 3; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); + this.Battery.ChargingState = this.hap.Characteristic.ChargingState.CHARGING; break; case 5: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 4; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 4; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); + this.Battery.ChargingState = this.hap.Characteristic.ChargingState.CHARGING; break; case 6: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 5; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 5; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); + this.Battery.ChargingState = this.hap.Characteristic.ChargingState.CHARGING; break; case 7: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 6; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 6; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); + this.Battery.ChargingState = this.hap.Characteristic.ChargingState.CHARGING; break; case 8: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 7; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 7; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); + this.Battery.ChargingState = this.hap.Characteristic.ChargingState.CHARGING; break; case 9: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 8; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel}`); + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 8; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel}`); + this.Battery.ChargingState = this.hap.Characteristic.ChargingState.CHARGING; break; case 10: default: - this.CurrentAmbientLightLevel = this.set_maxLux; + this.LightSensor!.CurrentAmbientLightLevel = set_maxLux; this.debugLog(); } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel},` + - ` CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel},` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } // Battery - this.BatteryLevel = Number(this.BLE_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(serviceData.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` + ` StatusLowBattery: ${this.StatusLowBattery}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); } - async openAPIparseStatus(): Promise { + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); // CurrentPosition - this.CurrentPosition = 100 - Number(this.OpenAPI_CurrentPosition); + this.WindowCovering!.CurrentPosition = 100 - Number(deviceStatus.body.slidePosition); await this.setMinMax(); - this.debugLog(`Curtain ${this.accessory.displayName} CurrentPosition: ${this.CurrentPosition}`); + this.debugLog(`Curtain ${this.accessory.displayName} CurrentPosition: ${this.WindowCovering.CurrentPosition}`); if (this.setNewTarget) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Checking Status ...`); } - if (this.setNewTarget && this.OpenAPI_InMotion) { + if (this.setNewTarget && deviceStatus.body.moving) { await this.setMinMax(); - if (Number(this.TargetPosition) > this.CurrentPosition) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Closing, CurrentPosition: ${this.CurrentPosition} `); - this.PositionState = this.hap.Characteristic.PositionState.INCREASING; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} INCREASING PositionState: ${this.PositionState}`); - } else if (Number(this.TargetPosition) < this.CurrentPosition) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Opening, CurrentPosition: ${this.CurrentPosition} `); - this.PositionState = this.hap.Characteristic.PositionState.DECREASING; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} DECREASING PositionState: ${this.PositionState}`); + if (Number(this.WindowCovering.TargetPosition) > this.WindowCovering.CurrentPosition) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Closing, CurrentPosition: ${this.WindowCovering.CurrentPosition} `); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.INCREASING; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} INCREASING` + + ` PositionState: ${this.WindowCovering.PositionState}`); + } else if (Number(this.WindowCovering.TargetPosition) < this.WindowCovering.CurrentPosition) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Opening, CurrentPosition: ${this.WindowCovering.CurrentPosition} `); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.DECREASING; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} DECREASING` + + ` PositionState: ${this.WindowCovering.PositionState}`); } else { - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} Standby, CurrentPosition: ${this.CurrentPosition}`); - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); - this.debugLog(`${this.device.deviceType}: ${this.CurrentPosition} STOPPED PositionState: ${this.PositionState}`); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} Standby,` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.STOPPED; + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); + this.debugLog(`${this.device.deviceType}: ${this.WindowCovering.CurrentPosition} STOPPED` + + ` PositionState: ${this.WindowCovering.PositionState}`); } } else { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Standby, CurrentPosition: ${this.CurrentPosition}`); - this.TargetPosition = this.CurrentPosition; - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Standby, CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + this.WindowCovering.TargetPosition = this.WindowCovering.CurrentPosition; + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.STOPPED; this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Stopped`); } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.CurrentPosition},` + - ` TargetPosition: ${this.TargetPosition}, PositionState: ${this.PositionState},`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.WindowCovering.CurrentPosition},` + + ` TargetPosition: ${this.WindowCovering.TargetPosition}, PositionState: ${this.WindowCovering.PositionState},`); // Brightness if (!this.device.curtain?.hide_lightsensor) { - this.set_minLux = this.minLux(); - this.set_maxLux = this.maxLux(); - switch (this.OpenAPI_CurrentAmbientLightLevel) { - case 'dim': - this.CurrentAmbientLightLevel = this.set_minLux; - break; + const set_minLux = this.device.curtain?.set_minLux ?? 1; + const set_maxLux = this.device.curtain?.set_maxLux ?? 6001; + switch (deviceStatus.body.brightness) { case 'bright': + this.LightSensor!.CurrentAmbientLightLevel = set_maxLux; + this.Battery.ChargingState = this.hap.Characteristic.ChargingState.CHARGING; + break; + case 'dim': default: - this.CurrentAmbientLightLevel = this.set_maxLux; + this.LightSensor!.CurrentAmbientLightLevel = set_minLux; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } // BatteryLevel - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - if (Number.isNaN(this.BatteryLevel)) { - this.BatteryLevel = 100; + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` - + ` StatusLowBattery: ${this.StatusLowBattery}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); + } } async refreshStatus(): Promise { @@ -520,10 +499,8 @@ export class Curtain { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -538,118 +515,45 @@ export class Curtain { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE Address: ${this.device.bleMac}`); this.getCustomBLEAddress(switchbot); // Start to monitor advertisement packets - let model: string; - switch (this.device.configDeviceType) { - case 'Curtain3': - model = '{'; - break; - default: - model = 'c'; - } (async () => { // Start to monitor advertisement packets - await switchbot.startScan({ - model: model, - id: this.device.bleMac, - }); + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === 'c') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_Calibration = ad.serviceData.calibration; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.BLE_InMotion = ad.serviceData.inMotion; - this.BLE_CurrentPosition = ad.serviceData.position; - this.BLE_CurrentAmbientLightLevel = ad.serviceData.lightLevel; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - - - /*if (switchbot !== false) { - switchbot - .startScan({ - model: model, - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.BLE_Calibration = ad.serviceData.calibration; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.BLE_InMotion = ad.serviceData.inMotion; - this.BLE_CurrentPosition = ad.serviceData.position; - this.BLE_CurrentAmbientLightLevel = ad.serviceData.lightLevel; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} calibration: ${ad.serviceData.calibration}, ` + - `position: ${ad.serviceData.position}, lightLevel: ${ad.serviceData.lightLevel}, battery: ${ad.serviceData.battery}, ` + - `inMotion: ${ad.serviceData.inMotion}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_CurrentPosition = deviceStatus.body.slidePosition; - this.OpenAPI_InMotion = deviceStatus.body.moving; - this.OpenAPI_CurrentAmbientLightLevel = deviceStatus.body.brightness; - this.OpenAPI_BatteryLevel = deviceStatus.body.battery; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -657,10 +561,30 @@ export class Curtain { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { slidePosition, battery } = context; + this.debugLog(`${device.deviceType}: ${accessory.displayName} ` + + '(slidePosition, battery) = ' + + `Webhook:(${slidePosition}, ${battery}), ` + + `current:(${this.WindowCovering.CurrentPosition}, ${this.Battery.BatteryLevel})`); + this.WindowCovering.CurrentPosition = slidePosition; + this.Battery.BatteryLevel = battery; + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} ` + + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; } } @@ -673,9 +597,8 @@ export class Curtain { await this.openAPIpushChanges(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, pushChanges will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, pushChanges will not happen.`); } // Refresh the status from the API interval(15000) @@ -688,7 +611,7 @@ export class Curtain { async BLEpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`); - if (this.TargetPosition !== this.CurrentPosition) { + if (this.WindowCovering.TargetPosition !== this.WindowCovering.CurrentPosition) { const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -696,28 +619,28 @@ export class Curtain { .join(':') .toLowerCase(); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE Address: ${this.device.bleMac}`); - this.SilentPerformance(); - const adjustedMode = this.setPositionMode === '1' ? 0x01 : 0xff; - this.debugLog(`${this.accessory.displayName} Mode: ${this.Mode}`); + const { setPositionMode, Mode }: { setPositionMode: number; Mode: string; } = await this.setPerformance(); + const adjustedMode = setPositionMode === 1 ? 0x01 : 0xff; + this.debugLog(`${this.accessory.displayName} Mode: ${Mode}`); if (switchbot !== false) { try { const device_list = await switchbot.discover({ model: 'c', quick: true, id: this.device.bleMac }); - this.infoLog(`${this.accessory.displayName} Target Position: ${this.TargetPosition}`); + this.infoLog(`${this.accessory.displayName} Target Position: ${this.WindowCovering.TargetPosition}`); - await this.retry({ - max: this.maxRetry(), + await this.retryBLE({ + max: await this.maxRetryBLE(), fn: async () => { - await device_list[0].runToPos(100 - Number(this.TargetPosition), adjustedMode); + await device_list[0].runToPos(100 - Number(this.WindowCovering.TargetPosition), adjustedMode); }, }); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `TargetPosition: ${this.WindowCovering.TargetPosition} sent over BLE, sent successfully`); } catch (e) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify((e as Error).message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify((e as Error).message)}`); await this.BLEPushConnection(); throw new Error('Connection error'); } @@ -726,30 +649,8 @@ export class Curtain { await this.BLEPushConnection(); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges, CurrentPosition & TargetPosition Are the Same.` + - ` CurrentPosition: ${this.CurrentPosition}, TargetPosition ${this.TargetPosition}`, - ); - } - } - - async retry({ max, fn }: { max: number; fn: { (): any; (): Promise } }): Promise { - return fn().catch(async (e: any) => { - if (max === 0) { - throw e; - } - this.infoLog(e); - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Retrying`); - await sleep(1000); - return this.retry({ max: max - 1, fn }); - }); - } - - maxRetry(): number { - if (this.device.maxRetry) { - return this.device.maxRetry; - } else { - return 5; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges, CurrentPosition & TargetPosition Are the Same.` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}, TargetPosition ${this.WindowCovering.TargetPosition}`); } } @@ -758,24 +659,13 @@ export class Curtain { let parameter: string; let commandType: string; this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`); - if (this.TargetPosition !== this.CurrentPosition || this.device.disableCaching) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Pushing ${this.TargetPosition}`); - const adjustedTargetPosition = 100 - Number(this.TargetPosition); - if (Number(this.TargetPosition) > 50) { - this.setPositionMode = this.device.curtain?.setOpenMode; - } else { - this.setPositionMode = this.device.curtain?.setCloseMode; - } - if (this.setPositionMode === '1') { - this.Mode = 'Silent Mode'; - } else if (this.setPositionMode === '0') { - this.Mode = 'Performance Mode'; - } else { - this.Mode = 'Default Mode'; - } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Mode: ${this.Mode}`); - const adjustedMode = this.setPositionMode || 'ff'; - if (this.HoldPosition) { + if (this.WindowCovering.TargetPosition !== this.WindowCovering.CurrentPosition || this.device.disableCaching) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Pushing ${this.WindowCovering.TargetPosition}`); + const adjustedTargetPosition = 100 - Number(this.WindowCovering.TargetPosition); + const { setPositionMode, Mode }: { setPositionMode: number; Mode: string; } = await this.setPerformance(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Mode: ${Mode}`); + const adjustedMode = setPositionMode || 'ff'; + if (this.WindowCovering.HoldPosition) { command = 'pause'; parameter = 'default'; commandType = 'command'; @@ -804,22 +694,20 @@ export class Curtain { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No OpenAPI Changes, CurrentPosition & TargetPosition Are the Same.` + - ` CurrentPosition: ${this.CurrentPosition}, TargetPosition ${this.TargetPosition}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No OpenAPI Changes, CurrentPosition & TargetPosition Are the Same.` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}, TargetPosition ${this.WindowCovering.TargetPosition}`); } } @@ -827,50 +715,53 @@ export class Curtain { * Handle requests to set the value of the "Target Position" characteristic */ async TargetPositionSet(value: CharacteristicValue): Promise { - if (this.TargetPosition === this.accessory.context.TargetPosition) { + if (this.WindowCovering.TargetPosition === this.accessory.context.TargetPosition) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set TargetPosition: ${value}`); } else { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set TargetPosition: ${value}`); } // Set HoldPosition to false when TargetPosition is changed - this.HoldPosition = false; - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.HoldPosition, this.HoldPosition); + this.WindowCovering.HoldPosition = false; + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.HoldPosition, this.WindowCovering.HoldPosition); - this.TargetPosition = value; + this.WindowCovering.TargetPosition = value; if (this.device.mqttURL) { - this.mqttPublish('TargetPosition', this.TargetPosition); - this.mqttPublish('HoldPosition', this.HoldPosition); + this.mqttPublish('TargetPosition', this.WindowCovering.TargetPosition.toString()); + this.mqttPublish('HoldPosition', this.WindowCovering.HoldPosition.toString()); // Convert boolean to string } await this.setMinMax(); - if (value > this.CurrentPosition) { - this.PositionState = this.hap.Characteristic.PositionState.INCREASING; + if (value > this.WindowCovering.CurrentPosition) { + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.INCREASING; this.setNewTarget = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} value: ${value}, CurrentPosition: ${this.CurrentPosition}`); - } else if (value < this.CurrentPosition) { - this.PositionState = this.hap.Characteristic.PositionState.DECREASING; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} value: ${value},` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); + } else if (value < this.WindowCovering.CurrentPosition) { + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.DECREASING; this.setNewTarget = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} value: ${value}, CurrentPosition: ${this.CurrentPosition}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} value: ${value},` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); } else { - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; + this.WindowCovering.PositionState = this.hap.Characteristic.PositionState.STOPPED; this.setNewTarget = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} value: ${value}, CurrentPosition: ${this.CurrentPosition}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} value: ${value},` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); } - this.windowCoveringService.setCharacteristic(this.hap.Characteristic.PositionState, this.PositionState); - this.windowCoveringService.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.PositionState); + this.WindowCovering.Service.setCharacteristic(this.hap.Characteristic.PositionState, this.WindowCovering.PositionState); + this.WindowCovering.Service.getCharacteristic(this.hap.Characteristic.PositionState).updateValue(this.WindowCovering.PositionState); /** * If Curtain movement time is short, the moving flag from backend is always false. * The minimum time depends on the network control latency. */ clearTimeout(this.setNewTargetTimer); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateRate: ${this.updateRate}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateRate: ${this.deviceUpdateRate}`); if (this.setNewTarget) { this.setNewTargetTimer = setTimeout(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} setNewTarget ${this.setNewTarget} timeout`); this.setNewTarget = false; - }, this.updateRate * 1000); + }, this.deviceUpdateRate * 1000); } this.doCurtainUpdate.next(); } @@ -880,153 +771,107 @@ export class Curtain { */ async HoldPositionSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} HoldPosition: ${value}`); - this.HoldPosition = value; + this.WindowCovering.HoldPosition = value; this.doCurtainUpdate.next(); } async updateHomeKitCharacteristics(): Promise { await this.setMinMax(); - if (this.CurrentPosition === undefined || Number.isNaN(this.CurrentPosition)) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.CurrentPosition}`); + if (this.WindowCovering.CurrentPosition === undefined || Number.isNaN(this.WindowCovering.CurrentPosition)) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentPosition: ${this.WindowCovering.CurrentPosition}`); } else { + this.accessory.context.CurrentPosition = this.WindowCovering.CurrentPosition; + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, Number(this.WindowCovering.CurrentPosition)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentPosition: ${this.WindowCovering.CurrentPosition}`); if (this.device.mqttURL) { - this.mqttPublish('CurrentPosition', this.CurrentPosition); + this.mqttPublish('CurrentPosition', this.WindowCovering.CurrentPosition.toString()); // Convert to string } - this.accessory.context.CurrentPosition = this.CurrentPosition; - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.CurrentPosition, Number(this.CurrentPosition)); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic CurrentPosition: ${this.CurrentPosition}`); } - if (this.PositionState === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} PositionState: ${this.PositionState}`); + if (this.WindowCovering.PositionState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} PositionState: ${this.WindowCovering.PositionState}`); } else { if (this.device.mqttURL) { - this.mqttPublish('PositionState', this.PositionState); + this.mqttPublish('PositionState', this.WindowCovering.PositionState.toString()); } - this.accessory.context.PositionState = this.PositionState; - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.PositionState, Number(this.PositionState)); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic PositionState: ${this.PositionState}`); + this.accessory.context.PositionState = this.WindowCovering.PositionState; + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.PositionState, Number(this.WindowCovering.PositionState)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` PositionState: ${this.WindowCovering.PositionState}`); } - if (this.TargetPosition === undefined || Number.isNaN(this.TargetPosition)) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} TargetPosition: ${this.TargetPosition}`); + if (this.WindowCovering.TargetPosition === undefined || Number.isNaN(this.WindowCovering.TargetPosition)) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} TargetPosition: ${this.WindowCovering.TargetPosition}`); } else { if (this.device.mqttURL) { - this.mqttPublish('TargetPosition', this.TargetPosition); + this.mqttPublish('TargetPosition', this.WindowCovering.TargetPosition.toString()); } - this.accessory.context.TargetPosition = this.TargetPosition; - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.TargetPosition, Number(this.TargetPosition)); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: ${this.TargetPosition}`); + this.accessory.context.TargetPosition = this.WindowCovering.TargetPosition; + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, Number(this.WindowCovering.TargetPosition)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetPosition: ${this.WindowCovering.TargetPosition}`); } - if (this.HoldPosition === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} HoldPosition: ${this.HoldPosition}`); + if (this.WindowCovering.HoldPosition === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} HoldPosition: ${this.WindowCovering.HoldPosition}`); } else { if (this.device.mqttURL) { - this.mqttPublish('HoldPosition', this.HoldPosition); + this.mqttPublish('HoldPosition', this.WindowCovering.HoldPosition.toString()); } - this.accessory.context.HoldPosition = this.HoldPosition; - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.HoldPosition, this.HoldPosition); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic HoldPosition: ${this.HoldPosition}`); + this.accessory.context.HoldPosition = this.WindowCovering.HoldPosition; + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.HoldPosition, this.WindowCovering.HoldPosition); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` HoldPosition: ${this.WindowCovering.HoldPosition}`); } if (!this.device.curtain?.hide_lightsensor) { - if (this.CurrentAmbientLightLevel === undefined || Number.isNaN(this.CurrentAmbientLightLevel)) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + if (this.LightSensor!.CurrentAmbientLightLevel === undefined || Number.isNaN(this.LightSensor!.CurrentAmbientLightLevel)) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } else { if (this.device.mqttURL) { - this.mqttPublish('CurrentAmbientLightLevel', this.CurrentAmbientLightLevel); + this.mqttPublish('CurrentAmbientLightLevel', this.LightSensor!.CurrentAmbientLightLevel.toString()); } - this.accessory.context.CurrentAmbientLightLevel = this.CurrentAmbientLightLevel; - this.lightSensorService?.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.CurrentAmbientLightLevel); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` updateCharacteristic CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`, - ); + this.accessory.context.CurrentAmbientLightLevel = this.LightSensor!.CurrentAmbientLightLevel; + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.LightSensor!.CurrentAmbientLightLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` updateCharacteristic CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); if (this.device.history) { this.historyService?.addEntry({ time: Math.round(new Date().valueOf() / 1000), - lux: this.CurrentAmbientLightLevel, + lux: this.LightSensor!.CurrentAmbientLightLevel, }); } } } - if (this.BatteryLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}`); + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); } else { if (this.device.mqttURL) { - this.mqttPublish('BatteryLevel', this.BatteryLevel); + this.mqttPublish('BatteryLevel', this.Battery.BatteryLevel.toString()); } - this.accessory.context.BatteryLevel = this.BatteryLevel; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.BatteryLevel); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); } - if (this.StatusLowBattery === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); } else { if (this.device.mqttURL) { - this.mqttPublish('StatusLowBattery', this.StatusLowBattery); - } - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); - } - } - - /* - * Publish MQTT message for topics of - * 'homebridge-switchbot/curtain/xx:xx:xx:xx:xx:xx' - */ - mqttPublish(topic: string, message: any) { - const mac = this.device.deviceId - ?.toLowerCase() - .match(/[\s\S]{1,2}/g) - ?.join(':'); - const options = this.device.mqttPubOptions || {}; - this.mqttClient?.publish(`homebridge-switchbot/curtain/${mac}/${topic}`, `${message}`, options); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT message: ${topic}/${message} options:${JSON.stringify(options)}`); - } - - /* - * Setup MQTT hadler if URL is specified. - */ - async setupMqtt(device: device & devicesConfig): Promise { - if (device.mqttURL) { - try { - const { connectAsync } = asyncmqtt; - this.mqttClient = await connectAsync(device.mqttURL, device.mqttOptions || {}); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT connection has been established successfully.`); - this.mqttClient.on('error', (e: Error) => { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to publish MQTT messages. ${e}`); - }); - } catch (e) { - this.mqttClient = null; - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to establish MQTT connection. ${e}`); + this.mqttPublish('StatusLowBattery', this.Battery.StatusLowBattery.toString()); } + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); } - } - - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); + if (this.Battery.ChargingState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ChargingState: ${this.Battery.ChargingState}`); } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'c', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); + if (this.device.mqttURL) { + this.mqttPublish('ChargingState', this.Battery.ChargingState.toString()); + } + this.accessory.context.ChargingState = this.Battery.ChargingState; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.ChargingState, this.Battery.ChargingState); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` ChargingState: ${this.Battery.ChargingState}`); } } @@ -1046,364 +891,71 @@ export class Curtain { } } - async SilentPerformance() { - if (Number(this.TargetPosition) > 50) { + async setPerformance() { + let setPositionMode: number; + let Mode: string; + if (Number(this.WindowCovering.TargetPosition) > 50) { if (this.device.curtain?.setOpenMode === '1') { - this.setPositionMode = '1'; - this.Mode = 'Silent Mode'; + setPositionMode = 1; + Mode = 'Silent Mode'; + } else if (this.device.curtain?.setOpenMode === '0') { + setPositionMode = 0; + Mode = 'Performance Mode'; } else { - this.setPositionMode = '0'; - this.Mode = 'Performance Mode'; + setPositionMode = 0; + Mode = 'Default Mode'; } } else { if (this.device.curtain?.setCloseMode === '1') { - this.setPositionMode = '1'; - this.Mode = 'Silent Mode'; + setPositionMode = 1; + Mode = 'Silent Mode'; + } else if (this.device.curtain?.setOpenMode === '0') { + setPositionMode = 0; + Mode = 'Performance Mode'; } else { - this.setPositionMode = '0'; - this.Mode = 'Performance Mode'; + setPositionMode = 0; + Mode = 'Default Mode'; } } + return { setPositionMode, Mode }; } async setMinMax(): Promise { if (this.device.curtain?.set_min) { - if (Number(this.CurrentPosition) <= this.device.curtain?.set_min) { - this.CurrentPosition = 0; + if (Number(this.WindowCovering.CurrentPosition) <= this.device.curtain?.set_min) { + this.WindowCovering.CurrentPosition = 0; } } if (this.device.curtain?.set_max) { - if (Number(this.CurrentPosition) >= this.device.curtain?.set_max) { - this.CurrentPosition = 100; + if (Number(this.WindowCovering.CurrentPosition) >= this.device.curtain?.set_max) { + this.WindowCovering.CurrentPosition = 100; } } if (this.device.history) { const motion = this.accessory.getService(this.hap.Service.MotionSensor); - const state = Number(this.CurrentPosition) > 0 ? 1 : 0; + const state = Number(this.WindowCovering.CurrentPosition) > 0 ? 1 : 0; motion?.updateCharacteristic(this.hap.Characteristic.MotionDetected, state); } } - minStep(device: device & devicesConfig): number { - if (device.curtain?.set_minStep) { - this.set_minStep = device.curtain?.set_minStep; - } else { - this.set_minStep = 1; - } - return this.set_minStep; - } - - minLux(): number { - if (this.device.curtain?.set_minLux) { - this.set_minLux = this.device.curtain?.set_minLux; - } else { - this.set_minLux = 1; - } - return this.set_minLux; - } - - maxLux(): number { - if (this.device.curtain?.set_maxLux) { - this.set_maxLux = this.device.curtain?.set_maxLux; - } else { - this.set_maxLux = 6001; - } - return this.set_maxLux; - } - - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - if (this.updateRate > device.scanDuration) { - this.scanDuration = this.updateRate; - if (this.BLE) { - this.warnLog( - `${this.device.deviceType}: ` + - `${this.accessory.displayName} scanDuration is less than updateRate, overriding scanDuration with updateRate`, - ); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - } - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - if (this.updateRate > 1) { - this.scanDuration = this.updateRate; - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - } - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); } } async apiError(e: any): Promise { - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.PositionState, e); - this.windowCoveringService.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.PositionState, e); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.ChargingState, e); if (!this.device.curtain?.hide_lightsensor) { - this.lightSensorService?.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, e); - } - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); - //throw new this.platform.api.hap.HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - } - - async deviceContext() { - if (this.accessory.context.CurrentPosition === undefined) { - this.CurrentPosition = 0; - } else { - this.CurrentPosition = this.accessory.context.CurrentPosition; - } - - if (this.accessory.context.TargetPosition === undefined) { - this.TargetPosition = 0; - } else { - this.TargetPosition = this.accessory.context.TargetPosition; - } - - if (this.accessory.context.PositionState === undefined) { - this.PositionState = this.hap.Characteristic.PositionState.STOPPED; - } else { - this.PositionState = this.accessory.context.PositionState; - } - - if (!this.device.curtain?.hide_lightsensor) { - if (this.accessory.context.CurrentAmbientLightLevel !== undefined) { - this.CurrentAmbientLightLevel = this.accessory.context.CurrentAmbientLightLevel; - } - } - - if (this.BLE) { - if (this.accessory.context.BatteryLevel === undefined) { - this.BatteryLevel = 100; - } else { - this.BatteryLevel = this.accessory.context.BatteryLevel; - } - if (this.accessory.context.StatusLowBattery === undefined) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - } else { - this.StatusLowBattery = this.accessory.context.StatusLowBattery; - } - } - - if (this.device.history === true) { - // initialize when this accessory is newly created. - this.accessory.context.lastActivation = this.accessory.context.lastActivation ?? 0; - } else { - // removes cached values if history is turned off - delete this.accessory.context.lastActivation; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - // refreshRate - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, e); + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.StatusActive, e); } - // updateRate - if (device?.curtain?.updateRate) { - this.updateRate = device?.curtain?.updateRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Curtain updateRate: ${this.updateRate}`); - } else { - this.updateRate = 7; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default Curtain updateRate: ${this.updateRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.curtain) { - config = device.curtain; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.mqttURL !== undefined) { - config['mqttURL'] = device.mqttURL; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.maxRetry !== undefined) { - config['maxRetry'] = device.maxRetry; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; } } diff --git a/src/device/device.ts b/src/device/device.ts new file mode 100644 index 00000000..90d8b1b6 --- /dev/null +++ b/src/device/device.ts @@ -0,0 +1,755 @@ +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * device.ts: @switchbot/homebridge-switchbot. + */ + +import { hostname } from 'os'; +import asyncmqtt from 'async-mqtt'; +import { BlindTiltMappingMode, SwitchBotModel, SwitchBotBLEModel, sleep } from '../utils.js'; + +import type { MqttClient } from 'mqtt'; +import type { SwitchBotPlatform } from '../platform.js'; +import type { API, HAP, Logging, PlatformAccessory } from 'homebridge'; +import type { SwitchBotPlatformConfig, device, devicesConfig } from '../settings.js'; + +export abstract class deviceBase { + public readonly api: API; + public readonly log: Logging; + public readonly config!: SwitchBotPlatformConfig; + protected readonly hap: HAP; + + // Config + protected deviceLogging!: string; + protected deviceRefreshRate!: number; + protected deviceUpdateRate!: number; + protected devicePushRate!: number; + protected deviceMaxRetries!: number; + protected deviceDelayBetweenRetries!: number; + + // Connection + protected readonly BLE: boolean; + protected readonly OpenAPI: boolean; + + // Accsrroy Information + protected deviceModel!: SwitchBotModel; + protected deviceBLEModel!: SwitchBotBLEModel; + + // BLE + protected scanDuration!: number; + + // EVE history service handler + protected historyService?: any = null; + + //MQTT stuff + protected mqttClient: MqttClient | null = null; + + constructor( + protected readonly platform: SwitchBotPlatform, + protected accessory: PlatformAccessory, + protected device: device & devicesConfig, + ) { + this.api = this.platform.api; + this.log = this.platform.log; + this.config = this.platform.config; + this.hap = this.api.hap; + + // Connection + this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; + this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; + + this.getDeviceLogSettings(accessory, device); + this.getDeviceRateSettings(accessory, device); + this.getDeviceRetry(accessory, device); + this.getDeviceConfigSettings(accessory, device); + this.getDeviceContext(accessory, device); + this.setupMqtt(accessory, device); + this.scan(accessory, device); + + // Set accessory information + accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') + .setCharacteristic(this.hap.Characteristic.AppMatchingIdentifier, 'id1087374760') + .setCharacteristic(this.hap.Characteristic.Name, device.deviceName ?? accessory.displayName) + .setCharacteristic(this.hap.Characteristic.ConfiguredName, device.deviceName ?? accessory.displayName) + .setCharacteristic(this.hap.Characteristic.Model, device.model ?? accessory.context.model) + .setCharacteristic(this.hap.Characteristic.ProductData, device.deviceId ?? accessory.context.deviceId) + .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId); + } + + async getDeviceLogSettings(accessory: PlatformAccessory, device: device & devicesConfig): Promise { + if (this.platform.debugMode) { + this.deviceLogging = accessory.context.logging = 'debugMode'; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); + } else if (device.logging) { + this.deviceLogging = accessory.context.logging = device.logging; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); + } else if (this.config.logging) { + this.deviceLogging = accessory.context.logging = this.config.logging; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); + } else { + this.deviceLogging = accessory.context.logging = 'standard'; + this.debugWarnLog(`${this.device.deviceType}: ${accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); + } + } + + async getDeviceRateSettings(accessory: PlatformAccessory, device: device & devicesConfig): Promise { + // refreshRate + if (device.refreshRate) { + this.deviceRefreshRate = device.refreshRate; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); + } else if (this.config.options?.refreshRate) { + this.deviceRefreshRate = this.config.options.refreshRate; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); + } else { + this.deviceRefreshRate = 5; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Default refreshRate: ${this.deviceRefreshRate}`); + } + accessory.context.deviceRefreshRate = this.deviceRefreshRate; + // updateRate + if (device.updateRate) { + this.deviceUpdateRate = device.updateRate; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Device Config updateRate: ${this.deviceUpdateRate}`); + } else if (this.config.options?.updateRate) { + this.deviceUpdateRate = this.config.options.updateRate; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Platform Config updateRate: ${this.deviceUpdateRate}`); + } else { + this.deviceUpdateRate = 5; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Default updateRate: ${this.deviceUpdateRate}`); + } + accessory.context.deviceUpdateRate = this.deviceUpdateRate; + // pushRate + if (device.pushRate) { + this.devicePushRate = device.pushRate; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Device Config pushRate: ${this.deviceUpdateRate}`); + } else if (this.config.options?.pushRate) { + this.devicePushRate = this.config.options.pushRate; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Platform Config pushRate: ${this.deviceUpdateRate}`); + } else { + this.devicePushRate = 1; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Default pushRate: ${this.deviceUpdateRate}`); + } + accessory.context.devicePushRate = this.devicePushRate; + } + + async getDeviceRetry(accessory: PlatformAccessory, device: device & devicesConfig): Promise { + if (device.maxRetries) { + this.deviceMaxRetries = device.maxRetries; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Device Max Retries: ${this.deviceMaxRetries}`); + } else { + this.deviceMaxRetries = 5; // Maximum number of retries + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Max Retries Not Set, Using: ${this.deviceMaxRetries}`); + } + if (device.delayBetweenRetries) { + this.deviceDelayBetweenRetries = device.delayBetweenRetries * 1000; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Device Delay Between Retries: ${this.deviceDelayBetweenRetries}`); + } else { + this.deviceDelayBetweenRetries = 3000; // Delay between retries in milliseconds + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Delay Between Retries Not Set,` + + ` Using: ${this.deviceDelayBetweenRetries}`); + } + } + + async retryBLE({ max, fn }: { max: number; fn: { (): any; (): Promise } }): Promise { + return fn().catch(async (e: any) => { + if (max === 0) { + throw e; + } + this.infoLog(e); + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Retrying`); + await sleep(1000); + return this.retryBLE({ max: max - 1, fn }); + }); + } + + async maxRetryBLE(): Promise { + if (this.device.maxRetry) { + return this.device.maxRetry; + } else { + return 5; + } + } + + async scan(accessory: PlatformAccessory, device: device & devicesConfig): Promise { + if (device.scanDuration) { + if (this.deviceUpdateRate > device.scanDuration) { + this.scanDuration = this.deviceUpdateRate; + if (this.BLE) { + this.warnLog( + `${this.device.deviceType}: ` + + `${accessory.displayName} scanDuration is less than updateRate, overriding scanDuration with updateRate`); + } + } else { + this.scanDuration = accessory.context.scanDuration = device.scanDuration; + } + if (this.BLE) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); + } + } else { + if (this.deviceUpdateRate > 1) { + this.scanDuration = this.deviceUpdateRate; + } else { + this.scanDuration = accessory.context.scanDuration = 1; + } + if (this.BLE) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); + } + } + } + + async getDeviceConfigSettings(accessory: PlatformAccessory, device: device & devicesConfig): Promise { + const deviceConfig = {}; + if (device.logging !== 'standard') { + deviceConfig['logging'] = device.logging; + } + if (device.refreshRate !== 0) { + deviceConfig['refreshRate'] = device.refreshRate; + } + if (device.updateRate !== 0) { + deviceConfig['updateRate'] = device.updateRate; + } + if (device.scanDuration !== 0) { + deviceConfig['scanDuration'] = device.scanDuration; + } + if (device.offline === true) { + deviceConfig['offline'] = device.offline; + } + if (device.maxRetry !== 0) { + deviceConfig['maxRetry'] = device.maxRetry; + } + if (device.webhook === true) { + deviceConfig['webhook'] = device.webhook; + } + if (device.connectionType !== '') { + deviceConfig['connectionType'] = device.connectionType; + } + if (device.external === true) { + deviceConfig['external'] = device.external; + } + if (device.mqttURL !== '') { + deviceConfig['mqttURL'] = device.mqttURL; + } + if (device.maxRetries !== 0) { + deviceConfig['maxRetries'] = device.maxRetries; + } + if (device.delayBetweenRetries !== 0) { + deviceConfig['delayBetweenRetries'] = device.delayBetweenRetries; + } + let botConfig = {}; + if (device.bot) { + botConfig = device.bot; + } + let lockConfig = {}; + if (device.lock) { + lockConfig = device.lock; + } + let ceilinglightConfig = {}; + if (device.ceilinglight) { + ceilinglightConfig = device.ceilinglight; + } + let colorbulbConfig = {}; + if (device.colorbulb) { + colorbulbConfig = device.colorbulb; + } + let contactConfig = {}; + if (device.contact) { + contactConfig = device.contact; + } + let motionConfig = {}; + if (device.motion) { + motionConfig = device.motion; + } + let curtainConfig = {}; + if (device.curtain) { + curtainConfig = device.curtain; + } + let hubConfig = {}; + if (device.hub) { + hubConfig = device.hub; + } + let waterdetectorConfig = {}; + if (device.waterdetector) { + waterdetectorConfig = device.waterdetector; + } + let humidifierConfig = {}; + if (device.humidifier) { + humidifierConfig = device.humidifier; + } + let meterConfig = {}; + if (device.meter) { + meterConfig = device.meter; + } + let iosensorConfig = {}; + if (device.iosensor) { + iosensorConfig = device.iosensor; + } + let striplightConfig = {}; + if (device.striplight) { + striplightConfig = device.striplight; + } + let plugConfig = {}; + if (device.plug) { + plugConfig = device.plug; + } + let blindTiltConfig = {}; + if (device.blindTilt) { + if (device.blindTilt?.mode === undefined) { + blindTiltConfig['mode'] = BlindTiltMappingMode.OnlyUp; + } + blindTiltConfig = device.blindTilt; + } + const config = Object.assign({}, deviceConfig, botConfig, curtainConfig, waterdetectorConfig, striplightConfig, plugConfig, iosensorConfig, + meterConfig, humidifierConfig, hubConfig, lockConfig, ceilinglightConfig, colorbulbConfig, contactConfig, motionConfig, blindTiltConfig); + if (Object.entries(config).length !== 0) { + this.debugSuccessLog(`${this.device.deviceType}: ${accessory.displayName} Config: ${JSON.stringify(config)}`); + } + } + + /* + * Publish MQTT message for topics of + * 'homebridge-switchbot/${this.device.deviceType}/xx:xx:xx:xx:xx:xx' + */ + mqttPublish(message: string, topic?: string) { + const mac = this.device.deviceId + ?.toLowerCase() + .match(/[\s\S]{1,2}/g) + ?.join(':'); + const options = this.device.mqttPubOptions || {}; + let mqttTopic: string; + let mqttMessageTopic: string; + if (topic) { + mqttTopic = `/${topic}`; + mqttMessageTopic = `${topic}/`; + } else { + mqttTopic = ''; + mqttMessageTopic = ''; + } + this.mqttClient?.publish(`homebridge-switchbot/${this.device.deviceType}/${mac}${mqttTopic}`, `${message}`, options); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` MQTT message: ${mqttMessageTopic}${message} options:${JSON.stringify(options)}`); + } + + /* + * Setup MQTT hadler if URL is specified. + */ + async setupMqtt(accessory: PlatformAccessory, device: device & devicesConfig): Promise { + if (device.mqttURL) { + try { + const { connectAsync } = asyncmqtt; + this.mqttClient = await connectAsync(device.mqttURL, device.mqttOptions || {}); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT connection has been established successfully.`); + this.mqttClient.on('error', (e: Error) => { + this.errorLog(`${this.device.deviceType}: ${accessory.displayName} Failed to publish MQTT messages. ${e}`); + }); + } catch (e) { + this.mqttClient = null; + this.errorLog(`${this.device.deviceType}: ${accessory.displayName} Failed to establish MQTT connection. ${e}`); + } + } + } + + /* + * Setup EVE history graph feature if enabled. + */ + async setupHistoryService(accessory: PlatformAccessory, device: device & devicesConfig): Promise { + const mac = this.device + .deviceId!.match(/.{1,2}/g)! + .join(':') + .toLowerCase(); + this.historyService = device.history + ? new this.platform.fakegatoAPI('room', accessory, { + log: this.platform.log, + storage: 'fs', + filename: `${hostname().split('.')[0]}_${mac}_persist.json`, + }) + : null; + } + + async getCustomBLEAddress(switchbot: any) { + if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} customBLEaddress: ${this.device.customBLEaddress}`); + (async () => { + // Start to monitor advertisement packets + await switchbot.startScan({ model: this.device.bleModel }); + // Set an event handler + switchbot.onadvertisement = (ad: any) => { + this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); + }; + await sleep(10000); + // Stop to monitor + switchbot.stopScan(); + })(); + } + } + + async getDeviceContext(accessory: PlatformAccessory, device: device & devicesConfig): Promise { + // Set the accessory context + switch (device.deviceType) { + case 'Humidifier': + device.model = SwitchBotModel.Humidifier; + device.bleModel = SwitchBotBLEModel.Humidifier; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Hub Mini': + device.model = SwitchBotModel.HubMini; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Hub Plus': + device.model = SwitchBotModel.HubPlus; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Hub 2': + device.model = SwitchBotModel.Hub2; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Bot': + device.model = SwitchBotModel.Bot; + device.bleModel = SwitchBotBLEModel.Bot; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Meter': + device.model = SwitchBotModel.Meter; + device.bleModel = SwitchBotBLEModel.Meter; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'MeterPlus': + device.model = SwitchBotModel.MeterPlusUS; + device.bleModel = SwitchBotBLEModel.MeterPlus; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Meter Plus (JP)': + device.model = SwitchBotModel.MeterPlusJP; + device.bleModel = SwitchBotBLEModel.MeterPlus; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'WoIOSensor': + device.model = SwitchBotModel.OutdoorMeter; + device.bleModel = SwitchBotBLEModel.OutdoorMeter; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Water Detector': + device.model = SwitchBotModel.WaterDetector; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Motion Sensor': + device.model = SwitchBotModel.MotionSensor; + device.bleModel = SwitchBotBLEModel.MotionSensor; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Contact Sensor': + device.model = SwitchBotModel.ContactSensor; + device.bleModel = SwitchBotBLEModel.ContactSensor; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Curtain': + device.model = SwitchBotModel.Curtain; + device.bleModel = SwitchBotBLEModel.Curtain; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Curtain3': + device.model = SwitchBotModel.Curtain3; + device.bleModel = SwitchBotBLEModel.Curtain3; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Blind Tilt': + device.model = SwitchBotModel.BlindTilt; + device.bleModel = SwitchBotBLEModel.BlindTilt; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Plug': + device.model = SwitchBotModel.Plug; + device.bleModel = SwitchBotBLEModel.PlugMiniUS; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Plug Mini (US)': + device.model = SwitchBotModel.PlugMiniUS; + device.bleModel = SwitchBotBLEModel.PlugMiniUS; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Plug Mini (JP)': + device.model = SwitchBotModel.PlugMiniJP; + device.bleModel = SwitchBotBLEModel.PlugMiniJP; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Smart Lock': + device.model = SwitchBotModel.Lock; + device.bleModel = SwitchBotBLEModel.Lock; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Smart Lock Pro': + device.model = SwitchBotModel.LockPro; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Color Bulb': + device.model = SwitchBotModel.ColorBulb; + device.bleModel = SwitchBotBLEModel.ColorBulb; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'K10+': + device.model = SwitchBotModel.K10; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'WoSweeper': + device.model = SwitchBotModel.WoSweeper; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'WoSweeperMini': + device.model = SwitchBotModel.WoSweeperMini; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Robot Vacuum Cleaner S1': + device.model = SwitchBotModel.RobotVacuumCleanerS1; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Robot Vacuum Cleaner S1 Plus': + device.model = SwitchBotModel.RobotVacuumCleanerS1Plus; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Robot Vacuum Cleaner S10': + device.model = SwitchBotModel.RobotVacuumCleanerS10; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Ceiling Light': + device.model = SwitchBotModel.CeilingLight; + device.bleModel = SwitchBotBLEModel.CeilingLight; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Ceiling Light Pro': + device.model = SwitchBotModel.CeilingLightPro; + device.bleModel = SwitchBotBLEModel.CeilingLightPro; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Strip Light': + device.model = SwitchBotModel.StripLight; + device.bleModel = SwitchBotBLEModel.StripLight; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Indoor Cam': + device.model = SwitchBotModel.IndoorCam; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Remote': + device.model = SwitchBotModel.Remote; + device.bleModel = SwitchBotBLEModel.Remote; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'remote with screen+': + device.model = SwitchBotModel.UniversalRemote; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + case 'Battery Circulator Fan': + device.model = SwitchBotModel.BatteryCirculatorFan; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + break; + default: + device.model = SwitchBotModel.Unknown; + device.bleModel = SwitchBotBLEModel.Unknown; + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Model: ${device.model}, BLE Model: ${device.bleModel}`); + } + accessory.context.model = device.model; + + // Firmware Version + let deviceFirmwareVersion: string; + if (device.firmware) { + deviceFirmwareVersion = device.firmware; + this.debugSuccessLog(`${device.deviceType}: ${accessory.displayName} 1 FirmwareRevision: ${device.firmware}`); + } else if (device.version) { + deviceFirmwareVersion = device.version; + this.debugSuccessLog(`${device.deviceType}: ${accessory.displayName} 2 FirmwareRevision: ${device.version}`); + } else if (accessory.context.deviceVersion) { + deviceFirmwareVersion = accessory.context.deviceVersion; + this.debugSuccessLog(`${device.deviceType}: ${accessory.displayName} 3 FirmwareRevision: ${accessory.context.deviceVersion}`); + } else { + deviceFirmwareVersion = this.platform.version ?? '0.0.0'; + if (this.platform.version) { + this.debugSuccessLog(`${device.deviceType}: ${accessory.displayName} 4 FirmwareRevision: ${this.platform.version}`); + } else { + this.debugSuccessLog(`${device.deviceType}: ${accessory.displayName} 5 FirmwareRevision: ${deviceFirmwareVersion}`); + } + } + const version = deviceFirmwareVersion.toString(); + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + let deviceVersion: string; + if (version?.includes('.') === false) { + const replace = version?.replace(/^V|-.*$/g, ''); + const match = replace?.match(/.{1,1}/g); + const validVersion = match?.join('.'); + deviceVersion = validVersion ?? '0.0.0'; + } else { + deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + } + accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.SoftwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${device.deviceType}: ${accessory.displayName} deviceVersion: ${accessory.context.deviceVersion}`); + } + + async statusCode(statusCode: number): Promise { + if (statusCode === 171) { + const previousStatusCode = statusCode; + if (this.device.hubDeviceId === this.device.deviceId) { + statusCode = 161; + this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${previousStatusCode} is now statusCode: ` + + `${statusCode}, because the hubDeviceId: ${this.device.hubDeviceId} is set to the same as the deviceId: ` + + `${this.device.deviceId}, meaning the device is it's own hub.`); + } + if (this.device.hubDeviceId === '000000000000') { + statusCode = 161; + this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${previousStatusCode} is now statusCode: ` + + `${statusCode}, because the hubDeviceId: ${this.device.hubDeviceId} is set to the same as the deviceId: ` + + `${this.device.deviceId}, meaning the device is it's own hub.`); + } + } + switch (statusCode) { + case 151: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); + break; + case 152: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); + break; + case 160: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); + break; + case 161: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); + break; + case 171: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + + `Hub: ${this.device.hubDeviceId}`); + break; + case 190: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with` + + ` server, Or command format is invalid, statusCode: ${statusCode}`); + break; + case 100: + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); + break; + case 200: + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); + break; + case 400: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` + + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); + break; + case 401: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` + + `but the request has not been authenticated, statusCode: ${statusCode}`); + break; + case 403: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` + + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); + break; + case 404: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` + + `statusCode: ${statusCode}`); + break; + case 406: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` + + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); + break; + case 415: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` + + `header that is not supported by the server, statusCode: ${statusCode}`); + break; + case 422: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` + + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); + break; + case 429: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` + + `requests allowed for a given time window, statusCode: ${statusCode}`); + break; + case 500: + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` + + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); + break; + default: + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + + `${statusCode}, Submit Bugs Here: https://tinyurl.com/SwitchBotBug`); + } + } + + /** + * Logging for Device + */ + infoLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.info(String(...log)); + } + } + + successLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.success(String(...log)); + } + } + + debugSuccessLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging?.includes('debug')) { + this.log.success('[DEBUG]', String(...log)); + } + } + } + + warnLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.warn(String(...log)); + } + } + + debugWarnLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging?.includes('debug')) { + this.log.warn('[DEBUG]', String(...log)); + } + } + } + + errorLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.error(String(...log)); + } + } + + debugErrorLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging?.includes('debug')) { + this.log.error('[DEBUG]', String(...log)); + } + } + } + + debugLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging === 'debug') { + this.log.info('[DEBUG]', String(...log)); + } else { + this.log.debug(String(...log)); + } + } + } + + enablingDeviceLogging(): boolean { + return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + } +} \ No newline at end of file diff --git a/src/device/fan.ts b/src/device/fan.ts new file mode 100644 index 00000000..1b46e237 --- /dev/null +++ b/src/device/fan.ts @@ -0,0 +1,749 @@ +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * plug.ts: @switchbot/homebridge-switchbot. + */ +import { request } from 'undici'; +import { deviceBase } from './device.js'; +import { Devices } from '../settings.js'; +import { Subject, debounceTime, interval, skipWhile, take, tap } from 'rxjs'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; + +export class Fan extends deviceBase { + // Services + private Fan: { + Name: CharacteristicValue; + Service: Service; + Active: CharacteristicValue; + SwingMode: CharacteristicValue; + RotationSpeed: CharacteristicValue; + }; + + private Battery: { + Name: CharacteristicValue; + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + ChargingState: CharacteristicValue; + }; + + private LightBulb: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + Brightness: CharacteristicValue; + }; + + // Updates + fanUpdateInProgress!: boolean; + doFanUpdate!: Subject; + + constructor( + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, + ) { + super(platform, accessory, device); + // this is subject we use to track when we need to POST changes to the SwitchBot API + this.doFanUpdate = new Subject(); + this.fanUpdateInProgress = false; + + // Initialize Fan Service + accessory.context.Fan = accessory.context.Fan ?? {}; + this.Fan = { + Name: accessory.context.Fan.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Fanv2) ?? accessory.addService(this.hap.Service.Fanv2) as Service, + Active: accessory.context.Active ?? this.hap.Characteristic.Active.INACTIVE, + SwingMode: accessory.context.SwingMode ?? this.hap.Characteristic.SwingMode.SWING_DISABLED, + RotationSpeed: accessory.context.RotationSpeed ?? 0, + }; + accessory.context.Fan = this.Fan as object; + + // Initialize Fan Service + this.Fan.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Fan.Name) + .getCharacteristic(this.hap.Characteristic.Active) + .onGet(() => { + return this.Fan.Active; + }) + .onSet(this.ActiveSet.bind(this)); + + // Initialize Fan RotationSpeed Characteristic + this.Fan.Service + .getCharacteristic(this.hap.Characteristic.RotationSpeed) + .onGet(() => { + return this.Fan.RotationSpeed; + }) + .onSet(this.RotationSpeedSet.bind(this)); + + // Initialize Fan SwingMode Characteristic + this.Fan.Service.getCharacteristic(this.hap.Characteristic.SwingMode) + .onGet(() => { + return this.Fan.SwingMode; + }) + .onSet(this.SwingModeSet.bind(this)); + + // Initialize Battery Service + accessory.context.Battery = accessory.context.Battery ?? {}; + this.Battery = { + Name: accessory.context.Battery.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + ChargingState: accessory.context.ChargingState ?? this.hap.Characteristic.ChargingState.NOT_CHARGING, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Service + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .getCharacteristic(this.hap.Characteristic.BatteryLevel) + .onGet(() => { + return this.Battery.BatteryLevel; + }); + + // Initialize Battery ChargingState Characteristic + this.Battery.Service + .getCharacteristic(this.hap.Characteristic.ChargingState) + .onGet(() => { + return this.Battery.ChargingState; + }); + + // Initialize Battery StatusLowBattery Characteristic + this.Battery.Service + .getCharacteristic(this.hap.Characteristic.StatusLowBattery) + .onGet(() => { + return this.Battery.StatusLowBattery; + }); + + // Initialize LightBulb Service + accessory.context.LightBulb = accessory.context.LightBulb ?? {}; + this.LightBulb = { + Name: accessory.context.LightBulb.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Lightbulb) ?? accessory.addService(this.hap.Service.Lightbulb) as Service, + On: accessory.context.On ?? false, + Brightness: accessory.context.Brightness ?? 0, + }; + accessory.context.LightBulb = this.LightBulb as object; + + // Initialize LightBulb Characteristics + this.LightBulb.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LightBulb.Name) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.LightBulb.On; + }) + .onSet(this.OnSet.bind(this)); + + // Initialize LightBulb Brightness Characteristic + this.LightBulb.Service + .getCharacteristic(this.hap.Characteristic.Brightness) + .onGet(() => { + return this.LightBulb.Brightness; + }) + .onSet(this.BrightnessSet.bind(this)); + + // Retrieve initial values and updateHomekit + this.refreshStatus(); + + // Update Homekit + this.updateHomeKitCharacteristics(); + + // Start an update interval + interval(this.deviceRefreshRate * 1000) + .pipe(skipWhile(() => this.fanUpdateInProgress)) + .subscribe(async () => { + await this.refreshStatus(); + }); + + //regisiter webhook event handler + this.registerWebhook(accessory, device); + + // Watch for Plug change events + // We put in a debounce of 100ms so we don't make duplicate calls + this.doFanUpdate + .pipe( + tap(() => { + this.fanUpdateInProgress = true; + }), + debounceTime(this.devicePushRate * 1000), + ) + .subscribe(async () => { + try { + await this.pushChanges(); + } catch (e: any) { + this.apiError(e); + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed pushChanges with ${device.connectionType} Connection,` + + ` Error Message: ${JSON.stringify(e.message)}`); + } + this.fanUpdateInProgress = false; + }); + } + + async BLEparseStatus(serviceData: serviceData): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); + + // State + switch (serviceData.state) { + case 'on': + this.Fan.Active = true; + break; + default: + this.Fan.Active = false; + } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Fan.Active}`); + } + + async openAPIparseStatus(deviceStatus: deviceStatus) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); + + // Active + this.Fan.Active = deviceStatus.body.power === 'on' ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.Fan.Active}`); + + // SwingMode + this.Fan.SwingMode = deviceStatus.body.oscillation === 'on' ? + this.hap.Characteristic.SwingMode.SWING_ENABLED : this.hap.Characteristic.SwingMode.SWING_DISABLED; + + // RotationSpeed + this.Fan.RotationSpeed = deviceStatus.body.fanSpeed; + + // ChargingState + this.Battery.ChargingState = deviceStatus.body.chargingStatus === 'charging' ? + this.hap.Characteristic.ChargingState.CHARGING : this.hap.Characteristic.ChargingState.NOT_CHARGING; + + // BatteryLevel + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + } else { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + } + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; + } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); + + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); + } + } + + + + /** + * Asks the SwitchBot API for the latest device information + */ + async refreshStatus(): Promise { + if (!this.device.enableCloudService && this.OpenAPI) { + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} refreshStatus enableCloudService: ${this.device.enableCloudService}`); + } else if (this.BLE) { + await this.BLERefreshStatus(); + } else if (this.OpenAPI && this.platform.config.credentials?.token) { + await this.openAPIRefreshStatus(); + } else { + await this.offlineOff(); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); + } + } + + async BLERefreshStatus(): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLERefreshStatus`); + const switchbot = await this.platform.connectBLE(); + // Convert to BLE Address + this.device.bleMac = this.device + .deviceId!.match(/.{1,2}/g)! + .join(':') + .toLowerCase(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE Address: ${this.device.bleMac}`); + this.getCustomBLEAddress(switchbot); + // Start to monitor advertisement packets + (async () => { + // Start to monitor advertisement packets + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); + // Set an event handler + switchbot.onadvertisement = (ad: any) => { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); + } else { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); + } + }; + // Wait 10 seconds + await switchbot.wait(this.scanDuration * 1000); + // Stop to monitor + await switchbot.stopScan(); + // Update HomeKit + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); + await this.updateHomeKitCharacteristics(); + })(); + if (switchbot === undefined) { + await this.BLERefreshConnection(switchbot); + } + } + + async openAPIRefreshStatus(): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); + try { + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); + const deviceStatus: any = await body.json(); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); + if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.openAPIparseStatus(deviceStatus); + this.updateHomeKitCharacteristics(); + } else { + this.statusCode(statusCode); + this.statusCode(deviceStatus.statusCode); + } + } catch (e: any) { + this.apiError(e); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { version, battery, powerState, oscillation, chargingStatus, fanSpeed } = context; + const { Active, SwingMode, RotationSpeed } = this.Fan; + const { BatteryLevel, ChargingState } = this.Battery; + const { FirmwareRevision } = accessory.context; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (version, battery, powerState, oscillation, chargingStatus, fanSpeed) = ` + + `Webhook:(${version}, ${battery}, ${powerState}, ${oscillation}, ${chargingStatus}, ${fanSpeed}), ` + + `current:(${FirmwareRevision}, ${BatteryLevel}, ${Active}, ${SwingMode}, ${ChargingState}, ${RotationSpeed})`); + + // Active + this.Fan.Active = powerState === 'ON' ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE; + + // SwingMode + this.Fan.SwingMode = oscillation === 'on' ? + this.hap.Characteristic.SwingMode.SWING_ENABLED : this.hap.Characteristic.SwingMode.SWING_DISABLED; + + // RotationSpeed + this.Fan.RotationSpeed = fanSpeed; + + // ChargingState + this.Battery.ChargingState = chargingStatus === 'charging' ? + this.hap.Characteristic.ChargingState.CHARGING : this.hap.Characteristic.ChargingState.NOT_CHARGING; + + // BatteryLevel + this.Battery.BatteryLevel = battery; + + // Firmware Version + if (version) { + accessory.context.version = version; + accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.version) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(accessory.context.version); + } + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); + } + } + + /** + * Pushes the requested changes to the SwitchBot API + * commandType command parameter Description + * "command" "turnOff" "default" = set to OFF state + * "command" "turnOn" "default" = set to ON state + * "command" "setNightLightMode" "off, 1, or 2" = off, turn off nightlight, (1, bright) (2, dim) + * "command" "setWindMode" "direct, natural, sleep, or baby" = Set fan mode + * "command" "setWindSpeed" "{1-100} e.g. 10" = Set fan speed 1~100 + */ + + async pushChanges(): Promise { + if (!this.device.enableCloudService && this.OpenAPI) { + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} pushChanges enableCloudService: ${this.device.enableCloudService}`); + } else if (this.BLE) { + await this.BLEpushChanges(); + } else if (this.OpenAPI && this.platform.config.credentials?.token) { + await this.openAPIpushChanges(); + } else { + await this.offlineOff(); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, pushChanges will not happen.`); + } + // Refresh the status from the API + interval(15000) + .pipe(skipWhile(() => this.fanUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.refreshStatus(); + }); + } + + async BLEpushChanges(): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`); + if (this.Fan.Active !== this.accessory.context.Active) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges` + + ` On: ${this.Fan.Active} OnCached: ${this.accessory.context.Active}`); + const switchbot = await this.platform.connectBLE(); + // Convert to BLE Address + this.device.bleMac = this.device + .deviceId!.match(/.{1,2}/g)! + .join(':') + .toLowerCase(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE Address: ${this.device.bleMac}`); + switchbot + .discover({ + model: this.device.bleModel, + id: this.device.bleMac, + }) + .then(async (device_list: any) => { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Fan.Active}`); + return await this.retryBLE({ + max: await this.maxRetryBLE(), + fn: async () => { + if (this.Fan.Active) { + return await device_list[0].turnOn({ id: this.device.bleMac }); + } else { + return await device_list[0].turnOff({ id: this.device.bleMac }); + } + }, + }); + }) + .then(() => { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `Active: ${this.Fan.Active} sent over BLE, sent successfully`); + this.Fan.Active = false; + }) + .catch(async (e: any) => { + this.apiError(e); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + await this.BLEPushConnection(); + }); + } else { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges,` + + ` Active: ${this.Fan.Active}, ActiveCached: ${this.accessory.context.Active}`); + } + } + + async openAPIpushChanges() { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`); + if (this.Fan.Active !== this.accessory.context.Active) { + let command = ''; + if (this.Fan.Active) { + command = 'turnOn'; + } else { + command = 'turnOff'; + } + const bodyChange = JSON.stringify({ + command: `${command}`, + parameter: 'default', + commandType: 'command', + }); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`); + try { + const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/commands`, { + body: bodyChange, + method: 'POST', + headers: this.platform.generateHeaders(), + }); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); + const deviceStatus: any = await body.json(); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus body: ${JSON.stringify(deviceStatus.body)}`); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); + if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { + this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); + } else { + this.statusCode(statusCode); + this.statusCode(deviceStatus.statusCode); + } + } catch (e: any) { + this.apiError(e); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } else { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.` + + `On: ${this.Fan.Active}, ActiveCached: ${this.accessory.context.Active}`); + } + // Push RotationSpeed Update + if (this.Fan.Active) { + await this.pushRotationSpeedChanges(); + } + // Push SwingMode Update + if (this.Fan.Active) { + await this.pushSwingModeChanges(); + } + } + + async pushRotationSpeedChanges(): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushRotationSpeedChanges`); + if (this.Fan.SwingMode !== this.accessory.context.SwingMode) { + const bodyChange = JSON.stringify({ + command: 'setWindSpeed', + parameter: `${this.Fan.RotationSpeed}`, + commandType: 'command', + }); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`); + try { + const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/commands`, { + body: bodyChange, + method: 'POST', + headers: this.platform.generateHeaders(), + }); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); + const deviceStatus: any = await body.json(); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus body: ${JSON.stringify(deviceStatus.body)}`); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); + if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { + this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); + } else { + this.statusCode(statusCode); + this.statusCode(deviceStatus.statusCode); + } + } catch (e: any) { + this.apiError(e); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushRotationSpeedChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } else { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, pushRotationSpeedChanges: ${this.Fan.RotationSpeed}, ` + + `pushRotationSpeedChangesCached: ${this.accessory.context.RotationSpeed}`); + } + } + + async pushSwingModeChanges(): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushSwingModeChanges`); + if (this.Fan.SwingMode !== this.accessory.context.SwingMode) { + let parameter = ''; + if (this.Fan.SwingMode === this.hap.Characteristic.SwingMode.SWING_ENABLED) { + parameter = 'on'; + } else { + parameter = 'off'; + } + const bodyChange = JSON.stringify({ + command: 'setOscillation', + parameter: `${parameter}`, + commandType: 'command', + }); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`); + try { + const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/commands`, { + body: bodyChange, + method: 'POST', + headers: this.platform.generateHeaders(), + }); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); + const deviceStatus: any = await body.json(); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus body: ${JSON.stringify(deviceStatus.body)}`); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); + if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { + this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); + } else { + this.statusCode(statusCode); + this.statusCode(deviceStatus.statusCode); + } + } catch (e: any) { + this.apiError(e); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushSwingModeChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } else { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, SwingMode: ${this.Fan.SwingMode}, ` + + `SwingModeCached: ${this.accessory.context.SwingMode}`); + } + } + + /** + * Handle requests to set the value of the "On" characteristic + */ + async ActiveSet(value: CharacteristicValue): Promise { + if (this.Fan.Active === this.accessory.context.Active) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Active: ${value}`); + } else { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Active: ${value}`); + } + + this.Fan.Active = value; + this.doFanUpdate.next(); + } + + /** + * Handle requests to set the value of the "On" characteristic + */ + async RotationSpeedSet(value: CharacteristicValue): Promise { + if (this.Fan.RotationSpeed === this.accessory.context.RotationSpeed) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set RotationSpeed: ${value}`); + } else { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set RotationSpeed: ${value}`); + } + + this.Fan.RotationSpeed = value; + this.doFanUpdate.next(); + } + + /** + * Handle requests to set the value of the "On" characteristic + */ + async SwingModeSet(value: CharacteristicValue): Promise { + if (this.Fan.SwingMode === this.accessory.context.SwingMode) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set SwingMode: ${value}`); + } else { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set SwingMode: ${value}`); + } + + this.Fan.SwingMode = value; + this.doFanUpdate.next(); + } + + /** + * Handle requests to set the value of the "On" characteristic + */ + async OnSet(value: CharacteristicValue): Promise { + if (this.LightBulb.On === this.accessory.context.On) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set On: ${value}`); + } else { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set On: ${value}`); + } + + this.LightBulb.On = value; + this.doFanUpdate.next(); + } + + /** + * Handle requests to set the value of the "Brightness" characteristic + */ + async BrightnessSet(value: CharacteristicValue): Promise { + if (this.LightBulb.Brightness === this.accessory.context.Brightness) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Brightness: ${value}`); + } else if (this.LightBulb.On) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`); + } else { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`); + } + + this.LightBulb.Brightness = value; + this.doFanUpdate.next(); + } + + async updateHomeKitCharacteristics(): Promise { + // Active + if (this.Fan.Active === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.Fan.Active}`); + } else { + this.accessory.context.Active = this.Fan.Active; + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, this.Fan.Active); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Fan.Active}`); + } + // RotationSpeed + if (this.Fan.RotationSpeed === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} RotationSpeed: ${this.Fan.RotationSpeed}`); + } else { + this.accessory.context.RotationSpeed = this.Fan.RotationSpeed; + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, this.Fan.RotationSpeed); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic RotationSpeed: ${this.Fan.RotationSpeed}`); + } + // SwingMode + if (this.Fan.SwingMode === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} SwingMode: ${this.Fan.SwingMode}`); + } else { + this.accessory.context.SwingMode = this.Fan.SwingMode; + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.SwingMode, this.Fan.SwingMode); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic SwingMode: ${this.Fan.SwingMode}`); + } + // BateryLevel + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); + } else { + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); + } + // ChargingState + if (this.Battery.ChargingState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ChargingState: ${this.Battery.ChargingState}`); + } else { + this.accessory.context.ChargingState = this.Battery.ChargingState; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.ChargingState, this.Battery.ChargingState); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic ChargingState: ${this.Battery.ChargingState}`); + } + // StatusLowBattery + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); + } else { + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); + } + } + + async BLEPushConnection() { + if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') { + this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Push Changes`); + await this.openAPIpushChanges(); + } + } + + async BLERefreshConnection(switchbot: any): Promise { + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} wasn't able to establish BLE Connection, node-switchbot:` + + ` ${JSON.stringify(switchbot)}`); + if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') { + this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Refresh Status`); + await this.openAPIRefreshStatus(); + } + } + + async offlineOff(): Promise { + if (this.device.offline) { + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.INACTIVE); + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, 0); + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.SwingMode, this.hap.Characteristic.SwingMode.SWING_DISABLED); + } + } + + async apiError(e: any): Promise { + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, e); + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, e); + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.SwingMode, e); + } +} diff --git a/src/device/hub.ts b/src/device/hub.ts index 3ebded2c..94b4bff0 100644 --- a/src/device/hub.ts +++ b/src/device/hub.ts @@ -1,106 +1,70 @@ -import asyncmqtt from 'async-mqtt'; -import { CharacteristicValue, PlatformAccessory, Service, Units, API, Logging, HAP } from 'homebridge'; -import { MqttClient } from 'mqtt'; -import { hostname } from 'os'; -import { interval } from 'rxjs'; -import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, device, deviceStatus, devicesConfig, SwitchBotPlatformConfig } from '../settings.js'; - -export class Hub { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * hub.ts: @switchbot/homebridge-switchbot. + */ +import { Units } from 'homebridge'; +import { deviceBase } from './device.js'; +import { Devices } from '../settings.js'; +import { convertUnits } from '../utils.js'; +import { Subject, interval, skipWhile } from 'rxjs'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { device, deviceStatus, devicesConfig } from '../settings.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; + +export class Hub extends deviceBase { // Services - lightSensorService?: Service; - humidityService?: Service; - temperatureService?: Service; - - // Characteristic Values - FirmwareRevision!: CharacteristicValue; - CurrentTemperature!: CharacteristicValue; - CurrentRelativeHumidity!: CharacteristicValue; - CurrentAmbientLightLevel!: CharacteristicValue; - - // OpenAPI Status - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_CurrentTemperature: deviceStatus['temperature']; - OpenAPI_CurrentRelativeHumidity: deviceStatus['humidity']; - OpenAPI_CurrentAmbientLightLevel!: deviceStatus['brightness']; - - // OpenAPI Others - spaceBetweenLevels!: number; - - //MQTT stuff - mqttClient: MqttClient | null = null; - - // EVE history service handler - historyService?: any; - - // Config - set_minStep!: number; - updateRate!: number; - set_minLux!: number; - set_maxLux!: number; - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; - - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; + private LightSensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentAmbientLightLevel: CharacteristicValue; + }; + + private HumiditySensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentRelativeHumidity: CharacteristicValue; + }; + + private TemperatureSensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentTemperature: CharacteristicValue; + }; + + // Updates + hubUpdateInProgress!: boolean; + doHubUpdate!: Subject; constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.refreshRate(device); - this.deviceContext(); - this.setupHistoryService(device); - this.setupMqtt(device); - this.deviceConfig(device); - - this.CurrentRelativeHumidity = accessory.context.CurrentRelativeHumidity; - this.CurrentTemperature = accessory.context.CurrentTemperature; + super(platform, accessory, device); + // this is subject we use to track when we need to POST changes to the SwitchBot API + this.doHubUpdate = new Subject(); + this.hubUpdateInProgress = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, accessory.context.model) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // Temperature Sensor Service + // Initialize Temperature Sensor Service if (device.hub?.hide_temperature) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Temperature Sensor Service`); - this.temperatureService = this.accessory.getService(this.hap.Service.TemperatureSensor); - accessory.removeService(this.temperatureService!); - } else if (!this.temperatureService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Temperature Sensor Service`); - const temperatureService = `${accessory.displayName} Temperature Sensor`; - (this.temperatureService = this.accessory.getService(this.hap.Service.TemperatureSensor) - || this.accessory.addService(this.hap.Service.TemperatureSensor)), temperatureService; - - this.temperatureService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Temperature Sensor`); - if (!this.temperatureService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.temperatureService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Temperature Sensor`); + if (this.TemperatureSensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Temperature Sensor Service`); + this.TemperatureSensor.Service = this.accessory.getService(this.hap.Service.TemperatureSensor) as Service; + accessory.removeService(this.TemperatureSensor.Service); } - this.temperatureService + } else { + accessory.context.TemperatureSensor = accessory.context.TemperatureSensor ?? {}; + this.TemperatureSensor = { + Name: accessory.context.TemperatureSensor.Name ?? `${accessory.displayName} Temperature Sensor`, + Service: accessory.getService(this.hap.Service.TemperatureSensor) ?? this.accessory.addService(this.hap.Service.TemperatureSensor) as Service, + CurrentTemperature: accessory.context.CurrentTemperature ?? 0, + }; + accessory.context.TemperatureSensor = this.TemperatureSensor as object; + + // Initialize Temperature Sensor Characteristic + this.TemperatureSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.TemperatureSensor.Name) .getCharacteristic(this.hap.Characteristic.CurrentTemperature) .setProps({ unit: Units['CELSIUS'], @@ -110,312 +74,128 @@ export class Hub { minStep: 0.1, }) .onGet(() => { - return this.CurrentTemperature!; + return this.TemperatureSensor!.CurrentTemperature; }); - } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Temperature Sensor Service Not Added`); } - // Humidity Sensor Service + // Initialize Humidity Sensor Service if (device.hub?.hide_humidity) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Humidity Sensor Service`); - this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor); - accessory.removeService(this.humidityService!); - } else if (!this.humidityService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Humidity Sensor Service`); - const humidityService = `${accessory.displayName} Humidity Sensor`; - (this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor) - || this.accessory.addService(this.hap.Service.HumiditySensor)), humidityService; - - this.humidityService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Humidity Sensor`); - if (!this.humidityService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.humidityService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Humidity Sensor`); + if (this.HumiditySensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Humidity Sensor Service`); + this.HumiditySensor.Service = this.accessory.getService(this.hap.Service.HumiditySensor) as Service; + accessory.removeService(this.HumiditySensor.Service); } - this.humidityService + } else { + accessory.context.HumiditySensor = accessory.context.HumiditySensor ?? {}; + this.HumiditySensor = { + Name: accessory.context.HumiditySensor.Name ?? `${accessory.displayName} Humidity Sensor`, + Service: accessory.getService(this.hap.Service.HumiditySensor) ?? this.accessory.addService(this.hap.Service.HumiditySensor) as Service, + CurrentRelativeHumidity: accessory.context.CurrentRelativeHumidity ?? 0, + }; + accessory.context.HumiditySensor = this.HumiditySensor as object; + + // Initialize Humidity Sensor Characteristics + this.HumiditySensor!.Service + .setCharacteristic(this.hap.Characteristic.Name, this.HumiditySensor.Name) .getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity) .setProps({ minStep: 0.1, }) .onGet(() => { - return this.CurrentRelativeHumidity; + return this.HumiditySensor!.CurrentRelativeHumidity; }); - } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Humidity Sensor Service Not Added`); } - // Light Sensor Service + // Initialize Light Sensor Service if (device.hub?.hide_lightsensor) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Light Sensor Service`); - this.lightSensorService = this.accessory.getService(this.hap.Service.LightSensor); - accessory.removeService(this.lightSensorService!); - } else if (!this.lightSensorService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Light Sensor Service`); - const lightSensorService = `${accessory.displayName} Light Sensor`; - (this.lightSensorService = this.accessory.getService(this.hap.Service.LightSensor) - || this.accessory.addService(this.hap.Service.LightSensor)), lightSensorService; - - this.lightSensorService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Light Sensor`); - this.lightSensorService.setCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Light Sensor`); + if (this.LightSensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Light Sensor Service`); + this.LightSensor.Service = this.accessory.getService(this.hap.Service.LightSensor) as Service; + accessory.removeService(this.LightSensor.Service); + } } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Light Sensor Service Not Added`); + accessory.context.LightSensor = accessory.context.LightSensor ?? {}; + this.LightSensor = { + Name: accessory.context.LightSensor.Name ?? `${accessory.displayName} Light Sensor`, + Service: accessory.getService(this.hap.Service.LightSensor) ?? this.accessory.addService(this.hap.Service.LightSensor) as Service, + CurrentAmbientLightLevel: accessory.context.CurrentAmbientLightLevel ?? 0.0001, + }; + accessory.context.LightSensor = this.LightSensor as object; + + // Initialize Light Sensor Characteristics + this.LightSensor!.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LightSensor.Name) + .getCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel) + .setProps({ + minStep: 1, + }) + .onGet(() => { + return this.LightSensor!.CurrentAmbientLightLevel; + }); } + // Retrieve initial values and updateHomekit + this.refreshStatus(); + // Retrieve initial values and update Homekit this.updateHomeKitCharacteristics(); // Start an update interval interval(this.deviceRefreshRate * 1000) + .pipe(skipWhile(() => this.hubUpdateInProgress)) .subscribe(async () => { await this.refreshStatus(); }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - if (context.scale === 'CELSIUS') { - const { temperature, humidity, lightLevel } = context; - const { CurrentTemperature, CurrentRelativeHumidity, CurrentAmbientLightLevel } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(temperature, humidity, lightLevel) = ' + - `Webhook:(${temperature}, ${humidity}, ${lightLevel}), ` + - `current:(${CurrentTemperature}, ${CurrentRelativeHumidity}, ${CurrentAmbientLightLevel})`); - this.CurrentRelativeHumidity = humidity; - this.CurrentTemperature = temperature; - this.set_minLux = this.minLux(); - this.set_maxLux = this.maxLux(); - this.spaceBetweenLevels = 19; - switch (lightLevel) { - case 1: - this.CurrentAmbientLightLevel = this.set_minLux; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 2: - this.CurrentAmbientLightLevel = (this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels; - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel},` + - ` Calculation: ${(this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels}`, - ); - break; - case 3: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 2; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 4: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 3; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 5: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 4; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 6: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 5; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 7: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 6; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 8: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 7; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 9: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 8; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 10: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 9; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 11: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 10; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 12: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 11; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 13: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 12; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 14: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 13; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 15: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 14; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 16: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 15; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 17: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 16; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 18: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 17; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 19: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 18; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 20: - default: - this.CurrentAmbientLightLevel = this.set_maxLux; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - } - this.updateHomeKitCharacteristics(); - } - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } - } + this.registerWebhook(accessory, device); - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE not supported`); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } } - async openAPIparseStatus(): Promise { + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); // CurrentRelativeHumidity if (!this.device.hub?.hide_humidity) { - this.CurrentRelativeHumidity = Number(this.OpenAPI_CurrentRelativeHumidity); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.CurrentRelativeHumidity}%`); + this.HumiditySensor!.CurrentRelativeHumidity = Number(deviceStatus.body.humidity); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.HumiditySensor!.CurrentRelativeHumidity}%`); } // CurrentTemperature if (!this.device.hub?.hide_temperature) { - this.CurrentTemperature = Number(this.OpenAPI_CurrentTemperature); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.CurrentTemperature}°c`); + this.TemperatureSensor!.CurrentTemperature = Number(deviceStatus.body.temperature); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.TemperatureSensor!.CurrentTemperature}°c`); } // Brightness if (!this.device.hub?.hide_lightsensor) { if (!this.device.curtain?.hide_lightsensor) { - this.set_minLux = this.minLux(); - this.set_maxLux = this.maxLux(); - this.spaceBetweenLevels = 19; - switch (this.OpenAPI_CurrentAmbientLightLevel) { - case 1: - this.CurrentAmbientLightLevel = this.set_minLux; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 2: - this.CurrentAmbientLightLevel = (this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels; - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel},` + - ` Calculation: ${(this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels}`, - ); - break; - case 3: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 2; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 4: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 3; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 5: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 4; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 6: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 5; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 7: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 6; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 8: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 7; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 9: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 8; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 10: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 9; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 11: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 10; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 12: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 11; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 13: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 12; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 14: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 13; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 15: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 14; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 16: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 15; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 17: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 16; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 18: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 17; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 19: - this.CurrentAmbientLightLevel = ((this.set_maxLux - this.set_minLux) / this.spaceBetweenLevels) * 18; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - break; - case 20: - default: - this.CurrentAmbientLightLevel = this.set_maxLux; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel}`); - } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.OpenAPI_CurrentAmbientLightLevel},` + - ` CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`, - ); + const set_minLux = this.device.curtain?.set_minLux ?? 1; + const set_maxLux = this.device.curtain?.set_maxLux ?? 6001; + const spaceBetweenLevels = 19; + const lightLevel = deviceStatus.body.lightLevel; + await this.getLightLevel(lightLevel, set_minLux, set_maxLux, spaceBetweenLevels); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${deviceStatus.body.lightLevel},` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } if (!this.device.hub?.hide_lightsensor) { - this.lightSensorService?.setCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.CurrentAmbientLightLevel); + this.LightSensor!.Service.setCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.LightSensor!.CurrentAmbientLightLevel); } } - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); + } } async refreshStatus(): Promise { @@ -432,21 +212,16 @@ export class Hub { async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_CurrentTemperature = deviceStatus.body.temperature; - this.OpenAPI_CurrentRelativeHumidity = deviceStatus.body.humidity; - this.OpenAPI_CurrentAmbientLightLevel = deviceStatus.body.lightLevel; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -454,10 +229,47 @@ export class Hub { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { + try { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { temperature, humidity, lightLevel } = context; + const { CurrentTemperature } = this.TemperatureSensor || { CurrentTemperature: undefined }; + const { CurrentAmbientLightLevel } = this.LightSensor || { CurrentAmbientLightLevel: undefined }; + const { CurrentRelativeHumidity } = this.HumiditySensor || { CurrentRelativeHumidity: undefined }; + if (context.scale !== 'CELCIUS' && device.hub?.convertUnitTo === undefined) { + this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} received a non-CELCIUS Webhook scale: ` + + `${context.scale}, Use the *convertUnitsTo* config under Hub settings, if displaying incorrectly in HomeKit.`); + } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + '(scale, temperature, humidity, lightLevel) = ' + + `Webhook:(${context.scale}, ${convertUnits(temperature, context.scale, device.hub?.convertUnitTo)}, ${humidity}, ${lightLevel}), ` + + `current:(${CurrentTemperature}, ${CurrentRelativeHumidity}, ${CurrentAmbientLightLevel})`); + if (!this.device.hub?.hide_humidity) { + this.HumiditySensor!.CurrentRelativeHumidity = humidity; + } + if (!this.device.hub?.hide_temperature) { + this.TemperatureSensor!.CurrentTemperature = convertUnits(temperature, context.scale, device.hub?.convertUnitTo); + } + if (!this.device.hub?.hide_lightsensor) { + const set_minLux = this.device.curtain?.set_minLux ?? 1; + const set_maxLux = this.device.curtain?.set_maxLux ?? 6001; + const spaceBetweenLevels = 19; + await this.getLightLevel(lightLevel, set_minLux, set_maxLux, spaceBetweenLevels); + } + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; } } @@ -471,56 +283,58 @@ export class Hub { // CurrentRelativeHumidity if (!this.device.hub?.hide_humidity) { - if (this.CurrentRelativeHumidity === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + if (this.HumiditySensor!.CurrentRelativeHumidity === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}`); } else { if (this.device.mqttURL) { - mqttmessage.push(`"humidity": ${this.CurrentRelativeHumidity}`); + mqttmessage.push(`"humidity": ${this.HumiditySensor!.CurrentRelativeHumidity}`); } if (this.device.history) { - entry['humidity'] = this.CurrentRelativeHumidity; + entry['humidity'] = this.HumiditySensor!.CurrentRelativeHumidity; } - this.accessory.context.CurrentRelativeHumidity = this.CurrentRelativeHumidity; - this.humidityService?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.CurrentRelativeHumidity); + this.accessory.context.CurrentRelativeHumidity = this.HumiditySensor!.CurrentRelativeHumidity; + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, + this.HumiditySensor!.CurrentRelativeHumidity); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `updateCharacteristic CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + + `updateCharacteristic CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}`); } } // CurrentTemperature if (!this.device.hub?.hide_temperature) { - if (this.CurrentTemperature === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}`); + if (this.TemperatureSensor!.CurrentTemperature === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); } else { if (this.device.mqttURL) { - mqttmessage.push(`"temperature": ${this.CurrentTemperature}`); + mqttmessage.push(`"temperature": ${this.TemperatureSensor!.CurrentTemperature}`); } if (this.device.history) { - entry['temp'] = this.CurrentTemperature; + entry['temp'] = this.TemperatureSensor!.CurrentTemperature; } - this.accessory.context.CurrentTemperature = this.CurrentTemperature; - this.temperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.CurrentTemperature); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.CurrentTemperature}`); + this.accessory.context.CurrentTemperature = this.TemperatureSensor!.CurrentTemperature; + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.TemperatureSensor!.CurrentTemperature); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); } } // CurrentAmbientLightLevel if (!this.device.hub?.hide_lightsensor) { - if (this.CurrentAmbientLightLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + if (this.LightSensor!.CurrentAmbientLightLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } else { if (this.device.mqttURL) { - mqttmessage.push(`"light": ${this.CurrentAmbientLightLevel}`); + mqttmessage.push(`"light": ${this.LightSensor!.CurrentAmbientLightLevel}`); } if (this.device.history) { - entry['lux'] = this.CurrentAmbientLightLevel; + entry['lux'] = this.LightSensor!.CurrentAmbientLightLevel; } - this.accessory.context.CurrentAmbientLightLevel = this.CurrentAmbientLightLevel; - this.lightSensorService?.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.CurrentAmbientLightLevel); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} ` - + `updateCharacteristic CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`, - ); + this.accessory.context.CurrentAmbientLightLevel = this.LightSensor!.CurrentAmbientLightLevel; + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.LightSensor!.CurrentAmbientLightLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `updateCharacteristic CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } } @@ -528,7 +342,7 @@ export class Hub { if (this.device.mqttURL) { this.mqttPublish(`{${mqttmessage.join(',')}}`); } - if (Number(this.CurrentRelativeHumidity) > 0) { + if (Number(this.HumiditySensor!.CurrentRelativeHumidity) > 0) { // reject unreliable data if (this.device.history) { this.historyService?.addEntry(entry); @@ -536,301 +350,117 @@ export class Hub { } } - /* - * Publish MQTT message for topics of - * 'homebridge-switchbot/meter/xx:xx:xx:xx:xx:xx' - */ - mqttPublish(message: any) { - const mac = this.device.deviceId - ?.toLowerCase() - .match(/[\s\S]{1,2}/g) - ?.join(':'); - const options = this.device.mqttPubOptions || {}; - this.mqttClient?.publish(`homebridge-switchbot/hub/${mac}`, `${message}`, options); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT message: ${message} options:${JSON.stringify(options)}`); - } - - /* - * Setup MQTT hadler if URL is specified. - */ - async setupMqtt(device: device & devicesConfig): Promise { - if (device.mqttURL) { - try { - const { connectAsync } = asyncmqtt; - this.mqttClient = await connectAsync(device.mqttURL, device.mqttOptions || {}); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT connection has been established successfully.`); - this.mqttClient.on('error', (e: Error) => { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to publish MQTT messages. ${e}`); - }); - } catch (e) { - this.mqttClient = null; - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to establish MQTT connection. ${e}`); + async offlineOff(): Promise { + if (this.device.offline) { + if (!this.device.hub?.hide_temperature) { + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.accessory.context.CurrentTemperature); + } + if (!this.device.hub?.hide_humidity) { + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, + this.accessory.context.CurrentRelativeHumidity); + } + if (!this.device.hub?.hide_lightsensor) { + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, + this.accessory.context.CurrentAmbientLightLevel); } } } - /* - * Setup EVE history graph feature if enabled. - */ - async setupHistoryService(device: device & devicesConfig): Promise { - const mac = this.device - .deviceId!.match(/.{1,2}/g)! - .join(':') - .toLowerCase(); - this.historyService = device.history - ? new this.platform.fakegatoAPI('custom', this.accessory, { - log: this.platform.log, - storage: 'fs', - filename: `${hostname().split('.')[0]}_${mac}_persist.json`, - }) - : null; + async apiError(e: any): Promise { + if (!this.device.hub?.hide_temperature) { + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); + } + if (!this.device.hub?.hide_humidity) { + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); + } + if (!this.device.hub?.hide_lightsensor) { + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, e); + } } - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); + async getLightLevel(lightLevel: any, set_minLux: number, set_maxLux: number, spaceBetweenLevels: number) { + switch (lightLevel) { + case 1: + this.LightSensor!.CurrentAmbientLightLevel = set_minLux; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); + case 2: + this.LightSensor!.CurrentAmbientLightLevel = (set_maxLux - set_minLux) / spaceBetweenLevels; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel},` + + ` Calculation: ${(set_maxLux - set_minLux) / spaceBetweenLevels}`); break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); + case 3: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 2; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); + case 4: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 3; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); + case 5: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 4; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); + case 6: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 5; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); + case 7: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 6; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); + case 8: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 7; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); + case 9: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 8; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); + case 10: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 9; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); + case 11: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 10; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); + case 12: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 11; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); + case 13: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 12; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); + case 14: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 13; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); + case 15: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 14; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); + case 16: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 15; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); + case 17: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 16; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); break; + case 18: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 17; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); + break; + case 19: + this.LightSensor!.CurrentAmbientLightLevel = ((set_maxLux - set_minLux) / spaceBetweenLevels) * 18; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); + break; + case 20: default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - - async offlineOff(): Promise { - if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); - } - } - - async apiError(e: any): Promise { - if (!this.device.hub?.hide_temperature) { - this.temperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); - } - if (!this.device.hub?.hide_humidity) { - this.humidityService?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); - } - if (!this.device.hub?.hide_lightsensor) { - this.lightSensorService?.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, e); - } - } - - minLux(): number { - if (this.device.curtain?.set_minLux) { - this.set_minLux = this.device.curtain?.set_minLux; - } else { - this.set_minLux = 1; + this.LightSensor!.CurrentAmbientLightLevel = set_maxLux; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${lightLevel}`); } - return this.set_minLux; - } - - maxLux(): number { - if (this.device.curtain?.set_maxLux) { - this.set_maxLux = this.device.curtain?.set_maxLux; - } else { - this.set_maxLux = 6001; - } - return this.set_maxLux; - } - - async deviceContext() { - if (this.CurrentRelativeHumidity === undefined) { - this.CurrentRelativeHumidity = 0; - } else { - this.CurrentRelativeHumidity = this.accessory.context.CurrentRelativeHumidity; - } - if (this.CurrentTemperature === undefined) { - this.CurrentTemperature = 0; - } else { - this.CurrentTemperature = this.accessory.context.CurrentTemperature; - } - if (this.CurrentAmbientLightLevel === undefined) { - this.CurrentAmbientLightLevel = this.set_minLux; - } else { - this.CurrentAmbientLightLevel = this.accessory.context.CurrentAmbientLightLevel; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - // refreshRate - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - // updateRate - if (device?.curtain?.updateRate) { - this.updateRate = device?.curtain?.updateRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Curtain updateRate: ${this.updateRate}`); - } else { - this.updateRate = 7; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default Curtain updateRate: ${this.updateRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.hub) { - config = device.hub; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; } } diff --git a/src/device/humidifier.ts b/src/device/humidifier.ts index b11d1fe2..945ea9a8 100644 --- a/src/device/humidifier.ts +++ b/src/device/humidifier.ts @@ -1,119 +1,74 @@ import { request } from 'undici'; -import { sleep } from '../utils.js'; +import { deviceBase } from './device.js'; import { interval, Subject } from 'rxjs'; -import { SwitchBotPlatform } from '../platform.js'; +import { Devices } from '../settings.js'; +import { convertUnits } from '../utils.js'; import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { Service, PlatformAccessory, CharacteristicValue, API, Logging, HAP } from 'homebridge'; -import { device, devicesConfig, serviceData, deviceStatus, Devices, SwitchBotPlatformConfig } from '../settings.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class Humidifier { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class Humidifier extends deviceBase { // Services - humidifierService: Service; - temperatureservice?: Service; - - // Characteristic Values - Active!: CharacteristicValue; - WaterLevel!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - CurrentTemperature!: CharacteristicValue; - CurrentRelativeHumidity!: CharacteristicValue; - TargetHumidifierDehumidifierState!: CharacteristicValue; - CurrentHumidifierDehumidifierState!: CharacteristicValue; - RelativeHumidityHumidifierThreshold!: CharacteristicValue; - - // OpenAPI - OpenAPI_Active: deviceStatus['power']; - OpenAPI_WaterLevel: deviceStatus['lackWater']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_CurrentTemperature: deviceStatus['temperature']; - OpenAPI_CurrentRelativeHumidity: deviceStatus['humidity']; - OpenAPI_CurrentHumidifierDehumidifierState: deviceStatus['auto']; - OpenAPI_RelativeHumidityHumidifierThreshold: deviceStatus['nebulizationEfficiency']; - - // BLE Others - connected?: boolean; - onState!: serviceData['onState']; - autoMode!: serviceData['autoMode']; - percentage!: serviceData['percentage']; - - // Config - set_minStep?: number; - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; + private HumidifierDehumidifier: { + Name: CharacteristicValue; + Service: Service; + Active: CharacteristicValue; + WaterLevel: CharacteristicValue; + CurrentRelativeHumidity: CharacteristicValue; + TargetHumidifierDehumidifierState: CharacteristicValue; + CurrentHumidifierDehumidifierState: CharacteristicValue; + RelativeHumidityHumidifierThreshold: CharacteristicValue; + }; + + private TemperatureSensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentTemperature: CharacteristicValue; + }; // Updates humidifierUpdateInProgress!: boolean; doHumidifierUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); - this.deviceContext(); - this.deviceConfig(device); - + super(platform, accessory, device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doHumidifierUpdate = new Subject(); this.humidifierUpdateInProgress = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, 'W0801800') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the service if it exists, otherwise create a new service - // you can create multiple services for each accessory - const humidifierService = `${accessory.displayName} Humidifier`; - (this.humidifierService = accessory.getService(this.hap.Service.HumidifierDehumidifier) - || accessory.addService(this.hap.Service.HumidifierDehumidifier)), humidifierService; - - this.humidifierService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.humidifierService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.humidifierService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/HumidifierDehumidifier - - // create handlers for required characteristics - this.humidifierService.setCharacteristic( - this.hap.Characteristic.CurrentHumidifierDehumidifierState, - this.CurrentHumidifierDehumidifierState, - ); - - this.humidifierService + // Initialize the HumidifierDehumidifier Service + accessory.context.HumidifierDehumidifier = accessory.context.HumidifierDehumidifier ?? {}; + this.HumidifierDehumidifier = { + Name: accessory.context.HumidifierDehumidifier.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.HumidifierDehumidifier) + ?? accessory.addService(this.hap.Service.HumidifierDehumidifier) as Service, + Active: accessory.context.Active ?? this.hap.Characteristic.Active.ACTIVE, + WaterLevel: accessory.context.WaterLevel ?? 100, + CurrentRelativeHumidity: accessory.context.CurrentRelativeHumidity ?? 50, + TargetHumidifierDehumidifierState: accessory.context.TargetHumidifierDehumidifierState + ?? this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER, + CurrentHumidifierDehumidifierState: accessory.context.CurrentHumidifierDehumidifierState + ?? this.hap.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE, + RelativeHumidityHumidifierThreshold: accessory.context.RelativeHumidityHumidifierThreshold ?? 50, + }; + accessory.context.HumidifierDehumidifier = this.HumidifierDehumidifier as object; + + // Initialize the HumidifierDehumidifier Characteristics + this.HumidifierDehumidifier.Service + .setCharacteristic(this.hap.Characteristic.Name, this.HumidifierDehumidifier.Name) + .setCharacteristic(this.hap.Characteristic.CurrentHumidifierDehumidifierState, + this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState) .getCharacteristic(this.hap.Characteristic.TargetHumidifierDehumidifierState) .setProps({ validValueRanges: [0, 1], @@ -121,36 +76,50 @@ export class Humidifier { maxValue: 1, validValues: [0, 1], }) + .onGet(() => { + return this.HumidifierDehumidifier.TargetHumidifierDehumidifierState; + }) .onSet(this.TargetHumidifierDehumidifierStateSet.bind(this)); - this.humidifierService.getCharacteristic(this.hap.Characteristic.Active).onSet(this.ActiveSet.bind(this)); + this.HumidifierDehumidifier.Service + .getCharacteristic(this.hap.Characteristic.Active) + .onGet(() => { + return this.HumidifierDehumidifier.Active; + }) + .onSet(this.ActiveSet.bind(this)); - this.humidifierService + this.HumidifierDehumidifier.Service .getCharacteristic(this.hap.Characteristic.RelativeHumidityHumidifierThreshold) .setProps({ validValueRanges: [0, 100], minValue: 0, maxValue: 100, - minStep: this.minStep(), + minStep: device.humidifier?.set_minStep ?? 1, + }) + .onGet(() => { + return this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold; }) .onSet(this.RelativeHumidityHumidifierThresholdSet.bind(this)); - // Temperature Sensor Service - if (device.humidifier?.hide_temperature || this.BLE) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Temperature Sensor Service`); - this.temperatureservice = this.accessory.getService(this.hap.Service.TemperatureSensor); - accessory.removeService(this.temperatureservice!); - } else if (!this.temperatureservice && !this.BLE) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Temperature Sensor Service`); - const temperatureservice = `${accessory.displayName} Temperature Sensor`; - (this.temperatureservice = this.accessory.getService(this.hap.Service.TemperatureSensor) - || this.accessory.addService(this.hap.Service.TemperatureSensor)), temperatureservice; - - this.temperatureservice.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Temperature Sensor`); - if (!this.temperatureservice.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.temperatureservice.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Temperature Sensor`); + // Initialize the Temperature Sensor Service + if (device.humidifier?.hide_temperature) { + if (this.TemperatureSensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Temperature Sensor Service`); + this.TemperatureSensor!.Service = this.accessory.getService(this.hap.Service.TemperatureSensor) as Service; + accessory.removeService(this.TemperatureSensor!.Service); } - this.temperatureservice + } else { + accessory.context.TemperatureSensor = accessory.context.TemperatureSensor ?? {}; + this.TemperatureSensor = { + Name: accessory.context.TemperatureSensor.Name ?? `${accessory.displayName} Temperature Sensor`, + Service: accessory.getService(this.hap.Service.TemperatureSensor) ?? this.accessory.addService(this.hap.Service.TemperatureSensor) as Service, + CurrentTemperature: accessory.context.CurrentTemperature || 30, + }; + accessory.context.TemperatureSensor = this.TemperatureSensor as object; + + // Initialize the Temperature Sensor Characteristics + this.TemperatureSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.TemperatureSensor.Name) .getCharacteristic(this.hap.Characteristic.CurrentTemperature) .setProps({ validValueRanges: [-273.15, 100], @@ -159,45 +128,25 @@ export class Humidifier { minStep: 0.1, }) .onGet(() => { - return this.CurrentTemperature; + return this.TemperatureSensor!.CurrentTemperature; }); - } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Temperature Sensor Service Not Added`); } + // Retrieve initial values and updateHomekit + this.refreshStatus(); + // Retrieve initial values and updateHomekit this.updateHomeKitCharacteristics(); // Start an update interval interval(this.deviceRefreshRate * 1000) .pipe(skipWhile(() => this.humidifierUpdateInProgress)) - .subscribe(() => { - this.refreshStatus(); + .subscribe(async () => { + await this.refreshStatus(); }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - if (context.scale === 'CELSIUS') { - const { temperature, humidity } = context; - const { CurrentTemperature, CurrentRelativeHumidity } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(temperature, humidity) = ' + - `Webhook:(${temperature}, ${humidity}), ` + - `current:(${CurrentTemperature}, ${CurrentRelativeHumidity})`); - this.CurrentRelativeHumidity = humidity; - this.CurrentTemperature = temperature; - this.updateHomeKitCharacteristics(); - } - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); // Watch for Humidifier change events // We put in a debounce of 100ms so we don't make duplicate calls @@ -206,121 +155,112 @@ export class Humidifier { tap(() => { this.humidifierUpdateInProgress = true; }), - debounceTime(this.platform.config.options!.pushRate! * 1000), + debounceTime(this.devicePushRate * 1000), ) .subscribe(async () => { try { await this.pushChanges(); } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + - ` Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + + ` Error Message: ${JSON.stringify(e.message)}`); } this.humidifierUpdateInProgress = false; }); } - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); + // Target Humidifier Dehumidifier State + if (serviceData.autoMode) { + this.HumidifierDehumidifier.TargetHumidifierDehumidifierState = this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER; + } // Current Relative Humidity - this.CurrentRelativeHumidity = this.percentage!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + this.HumidifierDehumidifier.CurrentRelativeHumidity = serviceData.percentage!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentRelativeHumidity: ${this.HumidifierDehumidifier.CurrentRelativeHumidity}`); // Active - if (this.onState) { - this.Active = this.hap.Characteristic.Active.ACTIVE; + if (serviceData.onState) { + this.HumidifierDehumidifier.Active = this.hap.Characteristic.Active.ACTIVE; } else { - this.Active = this.hap.Characteristic.Active.INACTIVE; + this.HumidifierDehumidifier.Active = this.hap.Characteristic.Active.INACTIVE; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.Active}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.HumidifierDehumidifier.Active}`); } - async openAPIparseStatus(): Promise { + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); // Current Relative Humidity - this.CurrentRelativeHumidity = this.OpenAPI_CurrentTemperature!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + this.HumidifierDehumidifier.CurrentRelativeHumidity = deviceStatus.body.temperature!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentRelativeHumidity: ${this.HumidifierDehumidifier.CurrentRelativeHumidity}`); // Current Temperature if (!this.device.humidifier?.hide_temperature) { - this.CurrentTemperature = this.OpenAPI_CurrentTemperature!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}`); + this.TemperatureSensor!.CurrentTemperature = deviceStatus.body.temperature!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); } // Target Humidifier Dehumidifier State - switch (this.OpenAPI_CurrentHumidifierDehumidifierState) { + switch (deviceStatus.body.auto) { case true: - this.TargetHumidifierDehumidifierState = this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER; - this.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING; - this.RelativeHumidityHumidifierThreshold = this.CurrentRelativeHumidity; + this.HumidifierDehumidifier.TargetHumidifierDehumidifierState = + this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER; + this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING; + this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold = this.HumidifierDehumidifier.CurrentRelativeHumidity; break; default: - this.TargetHumidifierDehumidifierState = this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER; - if (this.OpenAPI_RelativeHumidityHumidifierThreshold! > 100) { - this.RelativeHumidityHumidifierThreshold = 100; + this.HumidifierDehumidifier.TargetHumidifierDehumidifierState = this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER; + if (deviceStatus.body.nebulizationEfficiency! > 100) { + this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold = 100; } else { - this.RelativeHumidityHumidifierThreshold = this.OpenAPI_RelativeHumidityHumidifierThreshold!; + this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold = deviceStatus.body.nebulizationEfficiency!; } - if (this.CurrentRelativeHumidity > this.RelativeHumidityHumidifierThreshold) { - this.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.IDLE; - } else if (this.Active === this.hap.Characteristic.Active.INACTIVE) { - this.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE; + if (this.HumidifierDehumidifier.CurrentRelativeHumidity > this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold) { + this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.IDLE; + } else if (this.HumidifierDehumidifier.Active === this.hap.Characteristic.Active.INACTIVE) { + this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE; } else { - this.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING; + this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING; } } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + ` TargetHumidifierDehumidifierState: ${this.TargetHumidifierDehumidifierState}`, - ); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` RelativeHumidityHumidifierThreshold: ${this.RelativeHumidityHumidifierThreshold}`, - ); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + ` CurrentHumidifierDehumidifierState: ${this.CurrentHumidifierDehumidifierState}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` TargetHumidifierDehumidifierState: ${this.HumidifierDehumidifier.TargetHumidifierDehumidifierState}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` RelativeHumidityHumidifierThreshold: ${this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentHumidifierDehumidifierState: ${this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState}`); // Active - switch (this.OpenAPI_Active) { + switch (deviceStatus.body.power) { case 'on': - this.Active = this.hap.Characteristic.Active.ACTIVE; + this.HumidifierDehumidifier.Active = this.hap.Characteristic.Active.ACTIVE; break; default: - this.Active = this.hap.Characteristic.Active.INACTIVE; + this.HumidifierDehumidifier.Active = this.hap.Characteristic.Active.INACTIVE; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.Active}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.HumidifierDehumidifier.Active}`); // Water Level - if (this.OpenAPI_WaterLevel) { - this.WaterLevel = 0; + if (deviceStatus.body.lackWater) { + this.HumidifierDehumidifier.WaterLevel = 0; } else { - this.WaterLevel = 100; + this.HumidifierDehumidifier.WaterLevel = 100; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} WaterLevel: ${this.WaterLevel}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} WaterLevel: ${this.HumidifierDehumidifier.WaterLevel}`); - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); + } } - /** - * Asks the SwitchBot API for the latest device information - */ async refreshStatus(): Promise { if (!this.device.enableCloudService && this.OpenAPI) { this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} refreshStatus enableCloudService: ${this.device.enableCloudService}`); @@ -330,10 +270,8 @@ export class Humidifier { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -350,103 +288,43 @@ export class Humidifier { // Start to monitor advertisement packets (async () => { // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'e', - id: this.device.bleMac, - }); + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === 'e') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.autoMode = ad.serviceData.autoMode; - this.onState = ad.serviceData.onState; - this.percentage = ad.serviceData.percentage > 100 ? 100 : ad.serviceData.percentage; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: 'e', - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.autoMode = ad.serviceData.autoMode; - this.onState = ad.serviceData.onState; - this.percentage = ad.serviceData.percentage; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} model: ${ad.serviceData.model}, modelName: ${ad.serviceData.modelName},` + - `autoMode: ${ad.serviceData.autoMode}, onState: ${ad.serviceData.onState}, percentage: ${ad.serviceData.percentage}`, - ); - - if (ad.serviceData) { - this.connected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.connected}`); - await this.stopScanning(switchbot); - } else { - this.connected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.connected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_CurrentHumidifierDehumidifierState = deviceStatus.body.auto; - this.OpenAPI_Active = deviceStatus.body.power; - this.OpenAPI_WaterLevel = deviceStatus.body.lackWater; - this.OpenAPI_CurrentRelativeHumidity = deviceStatus.body.humidity; - this.OpenAPI_CurrentTemperature = deviceStatus.body.temperature; - this.OpenAPI_RelativeHumidityHumidifierThreshold = deviceStatus.body.nebulizationEfficiency; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -454,10 +332,32 @@ export class Humidifier { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { temperature, humidity } = context; + const { CurrentRelativeHumidity } = this.HumidifierDehumidifier; + const { CurrentTemperature } = this.TemperatureSensor || { CurrentTemperature: undefined }; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (temperature, humidity) = Webhook:(${convertUnits(temperature, + context.scale, device.iosensor?.convertUnitTo)}, ${humidity}), current:(${CurrentTemperature}, ${CurrentRelativeHumidity})`); + this.HumidifierDehumidifier.CurrentRelativeHumidity = humidity; + if (!device.humidifier?.hide_temperature) { + this.TemperatureSensor!.CurrentTemperature = convertUnits(temperature, context.scale, device.iosensor?.convertUnitTo); + } + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} ` + + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; } } @@ -473,9 +373,8 @@ export class Humidifier { await this.openAPIpushChanges(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, pushChanges will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, pushChanges will not happen.`); } interval(5000) .pipe(take(1)) @@ -500,18 +399,18 @@ export class Humidifier { id: this.device.bleMac, }) .then(async (device_list: any) => { - this.infoLog(`${this.accessory.displayName} Target Position: ${this.Active}`); - return await device_list[0].percentage(this.RelativeHumidityHumidifierThreshold); + this.infoLog(`${this.accessory.displayName} Active: ${this.HumidifierDehumidifier.Active}`); + return await device_list[0].percentage(this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold); }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `Active: ${this.HumidifierDehumidifier.Active} sent over BLE, sent successfully`); }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } @@ -519,13 +418,14 @@ export class Humidifier { async openAPIpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`); if ( - this.TargetHumidifierDehumidifierState === this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER && - this.Active === this.hap.Characteristic.Active.ACTIVE + this.HumidifierDehumidifier.TargetHumidifierDehumidifierState === this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER && + this.HumidifierDehumidifier.Active === this.hap.Characteristic.Active.ACTIVE ) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Pushing Manual: ${this.RelativeHumidityHumidifierThreshold}!`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` Pushing Manual: ${this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold}!`); const bodyChange = JSON.stringify({ command: 'setMode', - parameter: `${this.RelativeHumidityHumidifierThreshold}`, + parameter: `${this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold}`, commandType: 'command', }); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`); @@ -543,20 +443,21 @@ export class Humidifier { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else if ( - this.TargetHumidifierDehumidifierState === this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER && - this.Active === this.hap.Characteristic.Active.ACTIVE + this.HumidifierDehumidifier.TargetHumidifierDehumidifierState === + this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER && + this.HumidifierDehumidifier.Active === this.hap.Characteristic.Active.ACTIVE ) { await this.pushAutoChanges(); } else { @@ -570,8 +471,9 @@ export class Humidifier { async pushAutoChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushAutoChanges`); if ( - this.TargetHumidifierDehumidifierState === this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER && - this.Active === this.hap.Characteristic.Active.ACTIVE + this.HumidifierDehumidifier.TargetHumidifierDehumidifierState === + this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER && + this.HumidifierDehumidifier.Active === this.hap.Characteristic.Active.ACTIVE ) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Pushing Auto`); const bodyChange = JSON.stringify({ @@ -594,22 +496,20 @@ export class Humidifier { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushAutoChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushAutoChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No pushAutoChanges.` + - `TargetHumidifierDehumidifierState: ${this.TargetHumidifierDehumidifierState}, Active: ${this.Active}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushAutoChanges. TargetHumidifierDehumidifierState:` + + ` ${this.HumidifierDehumidifier.TargetHumidifierDehumidifierState}, Active: ${this.HumidifierDehumidifier.Active}`); } } @@ -618,7 +518,7 @@ export class Humidifier { */ async pushActiveChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushActiveChanges`); - if (this.Active === this.hap.Characteristic.Active.INACTIVE) { + if (this.HumidifierDehumidifier.Active === this.hap.Characteristic.Active.INACTIVE) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Pushing Off`); const bodyChange = JSON.stringify({ command: 'turnOff', @@ -640,19 +540,19 @@ export class Humidifier { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushActiveChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushActiveChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushActiveChanges. Active: ${this.Active}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushActiveChanges. Active: ${this.HumidifierDehumidifier.Active}`); } } @@ -660,13 +560,13 @@ export class Humidifier { * Handle requests to set the "Active" characteristic */ async ActiveSet(value: CharacteristicValue): Promise { - if (this.Active === this.accessory.context.Active) { + if (this.HumidifierDehumidifier.Active === this.accessory.context.Active) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Active: ${value}`); } else { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Active: ${value}`); } - this.Active = value; + this.HumidifierDehumidifier.Active = value; this.doHumidifierUpdate.next(); } @@ -674,13 +574,13 @@ export class Humidifier { * Handle requests to set the "Target Humidifier Dehumidifier State" characteristic */ async TargetHumidifierDehumidifierStateSet(value: CharacteristicValue): Promise { - if (this.Active === this.hap.Characteristic.Active.ACTIVE) { + if (this.HumidifierDehumidifier.Active === this.hap.Characteristic.Active.ACTIVE) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set TargetHumidifierDehumidifierState: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set TargetHumidifierDehumidifierState: ${value}`); } - this.TargetHumidifierDehumidifierState = value; + this.HumidifierDehumidifier.TargetHumidifierDehumidifierState = value; this.doHumidifierUpdate.next(); } @@ -688,16 +588,16 @@ export class Humidifier { * Handle requests to set the "Relative Humidity Humidifier Threshold" characteristic */ async RelativeHumidityHumidifierThresholdSet(value: CharacteristicValue): Promise { - if (this.Active === this.hap.Characteristic.Active.ACTIVE) { + if (this.HumidifierDehumidifier.Active === this.hap.Characteristic.Active.ACTIVE) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set RelativeHumidityHumidifierThreshold: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set RelativeHumidityHumidifierThreshold: ${value}`); } - this.RelativeHumidityHumidifierThreshold = value; - if (this.Active === this.hap.Characteristic.Active.INACTIVE) { - this.Active = this.hap.Characteristic.Active.ACTIVE; - this.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.IDLE; + this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold = value; + if (this.HumidifierDehumidifier.Active === this.hap.Characteristic.Active.INACTIVE) { + this.HumidifierDehumidifier.Active = this.hap.Characteristic.Active.ACTIVE; + this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.IDLE; } this.doHumidifierUpdate.next(); } @@ -706,117 +606,75 @@ export class Humidifier { * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics(): Promise { - if (this.CurrentRelativeHumidity === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + if (this.HumidifierDehumidifier.CurrentRelativeHumidity === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentRelativeHumidity: ${this.HumidifierDehumidifier.CurrentRelativeHumidity}`); } else { - this.humidifierService.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.CurrentRelativeHumidity); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + ` updateCharacteristic CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`, - ); - this.accessory.context.CurrentRelativeHumidity = this.CurrentRelativeHumidity; + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, + this.HumidifierDehumidifier.CurrentRelativeHumidity); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentRelativeHumidity: ${this.HumidifierDehumidifier.CurrentRelativeHumidity}`); + this.accessory.context.CurrentRelativeHumidity = this.HumidifierDehumidifier.CurrentRelativeHumidity; } if (this.OpenAPI) { - if (this.WaterLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} WaterLevel: ${this.WaterLevel}`); + if (this.HumidifierDehumidifier.WaterLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} WaterLevel: ${this.HumidifierDehumidifier.WaterLevel}`); } else { - this.humidifierService.updateCharacteristic(this.hap.Characteristic.WaterLevel, this.WaterLevel); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic WaterLevel: ${this.WaterLevel}`); - this.accessory.context.WaterLevel = this.WaterLevel; + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.WaterLevel, this.HumidifierDehumidifier.WaterLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` WaterLevel: ${this.HumidifierDehumidifier.WaterLevel}`); + this.accessory.context.WaterLevel = this.HumidifierDehumidifier.WaterLevel; } } - if (this.CurrentHumidifierDehumidifierState === undefined) { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` CurrentHumidifierDehumidifierState: ${this.CurrentHumidifierDehumidifierState}`, - ); + if (this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentHumidifierDehumidifierState: ${this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState}`); } else { - this.humidifierService.updateCharacteristic( - this.hap.Characteristic.CurrentHumidifierDehumidifierState, - this.CurrentHumidifierDehumidifierState, - ); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` updateCharacteristic CurrentHumidifierDehumidifierState: ${this.CurrentHumidifierDehumidifierState}`, - ); - this.accessory.context.CurrentHumidifierDehumidifierState = this.CurrentHumidifierDehumidifierState; - } - if (this.TargetHumidifierDehumidifierState === undefined) { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + ` TargetHumidifierDehumidifierState: ${this.TargetHumidifierDehumidifierState}`, - ); + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.CurrentHumidifierDehumidifierState, + this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentHumidifierDehumidifierState: ${this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState}`); + this.accessory.context.CurrentHumidifierDehumidifierState = this.HumidifierDehumidifier.CurrentHumidifierDehumidifierState; + } + if (this.HumidifierDehumidifier.TargetHumidifierDehumidifierState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` TargetHumidifierDehumidifierState: ${this.HumidifierDehumidifier.TargetHumidifierDehumidifierState}`); } else { - this.humidifierService.updateCharacteristic( - this.hap.Characteristic.TargetHumidifierDehumidifierState, - this.TargetHumidifierDehumidifierState, - ); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` updateCharacteristic TargetHumidifierDehumidifierState: ${this.TargetHumidifierDehumidifierState}`, - ); - this.accessory.context.TargetHumidifierDehumidifierState = this.TargetHumidifierDehumidifierState; - } - if (this.Active === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.Active}`); + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.TargetHumidifierDehumidifierState, + this.HumidifierDehumidifier.TargetHumidifierDehumidifierState); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetHumidifierDehumidifierState: ${this.HumidifierDehumidifier.TargetHumidifierDehumidifierState}`); + this.accessory.context.TargetHumidifierDehumidifierState = this.HumidifierDehumidifier.TargetHumidifierDehumidifierState; + } + if (this.HumidifierDehumidifier.Active === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Active: ${this.HumidifierDehumidifier.Active}`); } else { - this.humidifierService.updateCharacteristic(this.hap.Characteristic.Active, this.Active); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Active}`); - this.accessory.context.Active = this.Active; + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.Active, this.HumidifierDehumidifier.Active); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.HumidifierDehumidifier.Active}`); + this.accessory.context.Active = this.HumidifierDehumidifier.Active; } - if (this.RelativeHumidityHumidifierThreshold === undefined) { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` RelativeHumidityHumidifierThreshold: ${this.RelativeHumidityHumidifierThreshold}`, - ); + if (this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` RelativeHumidityHumidifierThreshold: ${this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold}`); } else { - this.humidifierService.updateCharacteristic( - this.hap.Characteristic.RelativeHumidityHumidifierThreshold, - this.RelativeHumidityHumidifierThreshold, - ); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` updateCharacteristic RelativeHumidityHumidifierThreshold: ${this.RelativeHumidityHumidifierThreshold}`, - ); - this.accessory.context.RelativeHumidityHumidifierThreshold = this.RelativeHumidityHumidifierThreshold; + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.RelativeHumidityHumidifierThreshold, + this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` RelativeHumidityHumidifierThreshold: ${this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold}`); + this.accessory.context.RelativeHumidityHumidifierThreshold = this.HumidifierDehumidifier.RelativeHumidityHumidifierThreshold; } if (!this.device.humidifier?.hide_temperature && !this.BLE) { - if (this.CurrentTemperature === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}`); + if (this.TemperatureSensor!.CurrentTemperature === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); } else { - this.temperatureservice?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.CurrentTemperature); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.CurrentTemperature}`); - this.accessory.context.CurrentTemperature = this.CurrentTemperature; + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.TemperatureSensor!.CurrentTemperature); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); + this.accessory.context.CurrentTemperature = this.TemperatureSensor!.CurrentTemperature; } } } - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.connected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); - } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'e', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); - } - } - async BLEPushConnection() { if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') { this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Push Changes`); @@ -833,283 +691,28 @@ export class Humidifier { } } - minStep(): number { - if (this.device.humidifier?.set_minStep) { - this.set_minStep = this.device.humidifier?.set_minStep; - } else { - this.set_minStep = 1; - } - return this.set_minStep; - } - - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} offline: ${this.device.offline}`); if (this.device.offline) { - await this.deviceContext(); - if (this.CurrentTemperature === undefined) { - this.CurrentTemperature = 0; - } - await this.updateHomeKitCharacteristics(); + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.CurrentHumidifierDehumidifierState, + this.hap.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE); + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.TargetHumidifierDehumidifierState, + this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER); + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.INACTIVE); } } async apiError(e: any): Promise { - this.humidifierService.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); if (!this.BLE) { - this.humidifierService.updateCharacteristic(this.hap.Characteristic.WaterLevel, e); + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.WaterLevel, e); } - this.humidifierService.updateCharacteristic(this.hap.Characteristic.CurrentHumidifierDehumidifierState, e); - this.humidifierService.updateCharacteristic(this.hap.Characteristic.TargetHumidifierDehumidifierState, e); - this.humidifierService.updateCharacteristic(this.hap.Characteristic.Active, e); - this.humidifierService.updateCharacteristic(this.hap.Characteristic.RelativeHumidityHumidifierThreshold, e); + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.CurrentHumidifierDehumidifierState, e); + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.TargetHumidifierDehumidifierState, e); + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.Active, e); + this.HumidifierDehumidifier.Service.updateCharacteristic(this.hap.Characteristic.RelativeHumidityHumidifierThreshold, e); if (!this.device.humidifier?.hide_temperature && !this.BLE) { - this.temperatureservice?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); - } - } - - async deviceContext() { - if (this.Active === undefined) { - this.Active = this.hap.Characteristic.Active.ACTIVE; - } else { - this.Active = this.accessory.context.Active; - } - if (this.CurrentTemperature === undefined) { - this.CurrentTemperature = 30; - } else { - this.CurrentTemperature = this.accessory.context.CurrentTemperature; - } - if (this.CurrentRelativeHumidity === undefined) { - this.CurrentRelativeHumidity = 0; - } else { - this.CurrentRelativeHumidity = this.accessory.context.CurrentRelativeHumidity; - } - if (this.TargetHumidifierDehumidifierState === undefined) { - this.TargetHumidifierDehumidifierState = this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER; - } else if (this.accessory.context.TargetHumidifierDehumidifierState === undefined) { - this.TargetHumidifierDehumidifierState = this.hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER; - } else { - this.TargetHumidifierDehumidifierState = this.accessory.context.TargetHumidifierDehumidifierState; - } - if (this.CurrentHumidifierDehumidifierState === undefined) { - this.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE; - } else if (this.accessory.context.CurrentHumidifierDehumidifierState === undefined) { - this.CurrentHumidifierDehumidifierState = this.hap.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE; - } else { - this.CurrentHumidifierDehumidifierState = this.accessory.context.CurrentHumidifierDehumidifierState; - } - if (this.RelativeHumidityHumidifierThreshold === undefined) { - this.RelativeHumidityHumidifierThreshold = 0; - } else { - this.RelativeHumidityHumidifierThreshold = this.accessory.context.RelativeHumidityHumidifierThreshold; - } - if (this.WaterLevel === undefined) { - this.WaterLevel = 0; - } else { - this.WaterLevel = this.accessory.context.WaterLevel; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); } } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.humidifier) { - config = device.humidifier; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.offline !== undefined) { - config['offline'] = device.offline; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - async infoLog(...log: any[]): Promise { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - async warnLog(...log: any[]): Promise { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - async debugWarnLog(...log: any[]): Promise { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - async errorLog(...log: any[]): Promise { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - async debugErrorLog(...log: any[]): Promise { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - async debugLog(...log: any[]): Promise { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; - } } diff --git a/src/device/iosensor.ts b/src/device/iosensor.ts index b987a701..3daaccfe 100644 --- a/src/device/iosensor.ts +++ b/src/device/iosensor.ts @@ -1,123 +1,102 @@ -import { hostname } from 'os'; -import { request } from 'undici'; -import { sleep } from '../utils.js'; -import { MqttClient } from 'mqtt'; -import asyncmqtt from 'async-mqtt'; -import { interval, Subject } from 'rxjs'; -import { skipWhile } from 'rxjs/operators'; -import { SwitchBotPlatform } from '../platform.js'; -import { Service, PlatformAccessory, Units, CharacteristicValue, API, Logging, HAP } from 'homebridge'; -import { device, devicesConfig, serviceData, temperature, deviceStatus, Devices, SwitchBotPlatformConfig } from '../settings.js'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * iosensor.ts: @switchbot/homebridge-switchbot. + */ +import { Units } from 'homebridge'; +import { deviceBase } from './device.js'; +import { Devices } from '../settings.js'; +import { convertUnits } from '../utils.js'; +import { Subject, interval, skipWhile } from 'rxjs'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class IOSensor { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class IOSensor extends deviceBase { // Services - batteryService: Service; - humidityservice?: Service; - temperatureservice?: Service; - - // Characteristic Values - BatteryLevel!: CharacteristicValue; - StatusLowBattery!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - CurrentTemperature?: CharacteristicValue; - CurrentRelativeHumidity!: CharacteristicValue; - - // OpenAPI Status - OpenAPI_BatteryLevel: deviceStatus['battery']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_CurrentTemperature: deviceStatus['temperature']; - OpenAPI_CurrentRelativeHumidity: deviceStatus['humidity']; - - // BLE Status - BLE_Celsius!: temperature['c']; - BLE_Fahrenheit!: temperature['f']; - BLE_BatteryLevel!: serviceData['battery']; - BLE_CurrentTemperature!: serviceData['temperature']; - BLE_CurrentRelativeHumidity!: serviceData['humidity']; - - // BLE Others - BLE_IsConnected?: boolean; - - //MQTT stuff - mqttClient: MqttClient | null = null; - - // EVE history service handler - historyService?: any; - - // Config - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; + private Battery: { + Name: CharacteristicValue; + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + }; + + private HumiditySensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentRelativeHumidity: CharacteristicValue; + }; + + private TemperatureSensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentTemperature: CharacteristicValue; + }; // Updates ioSensorUpdateInProgress!: boolean; doIOSensorUpdate: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); - this.deviceContext(); - this.setupHistoryService(device); - this.setupMqtt(device); - this.deviceConfig(device); - + super(platform, accessory, device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doIOSensorUpdate = new Subject(); this.ioSensorUpdateInProgress = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); + // Initialize Battery Service + accessory.context.Battery = accessory.context.Battery ?? {}; + this.Battery = { + Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Characteristics + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE) + .getCharacteristic(this.hap.Characteristic.BatteryLevel) + .onGet(() => { + return this.Battery.BatteryLevel; + }); - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, 'WoIOSensor') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // Temperature Sensor Service - if (device.meter?.hide_temperature) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Temperature Sensor Service`); - this.temperatureservice = this.accessory.getService(this.hap.Service.TemperatureSensor); - accessory.removeService(this.temperatureservice!); - } else if (!this.temperatureservice) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Temperature Sensor Service`); - const temperatureservice = `${accessory.displayName} Temperature Sensor`; - (this.temperatureservice = this.accessory.getService(this.hap.Service.TemperatureSensor) - || this.accessory.addService(this.hap.Service.TemperatureSensor)), temperatureservice; - - this.temperatureservice.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Temperature Sensor`); - if (!this.temperatureservice.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.temperatureservice.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Temperature Sensor`); + this.Battery.Service + .getCharacteristic(this.hap.Characteristic.StatusLowBattery) + .onGet(() => { + return this.Battery.StatusLowBattery; + }); + accessory.context.BatteryName = this.Battery.Name; + + // InitializeTemperature Sensor Service + if (device.iosensor?.hide_temperature) { + if (this.TemperatureSensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Temperature Sensor Service`); + this.TemperatureSensor.Service = this.accessory.getService(this.hap.Service.TemperatureSensor) as Service; + accessory.removeService(this.TemperatureSensor.Service); } - this.temperatureservice + } else { + accessory.context.TemperatureSensor = accessory.context.TemperatureSensor ?? {}; + this.TemperatureSensor = { + Name: accessory.context.TemperatureSensor.Name ?? `${accessory.displayName} Temperature Sensor`, + Service: accessory.getService(this.hap.Service.TemperatureSensor) ?? this.accessory.addService(this.hap.Service.TemperatureSensor) as Service, + CurrentTemperature: accessory.context.CurrentTemperature ?? 30, + }; + accessory.context.TemperatureSensor = this.TemperatureSensor as object; + + // Initialize Temperature Sensor Characteristics + this.TemperatureSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.TemperatureSensor.Name) .getCharacteristic(this.hap.Characteristic.CurrentTemperature) .setProps({ unit: Units['CELSIUS'], @@ -127,49 +106,40 @@ export class IOSensor { minStep: 0.1, }) .onGet(() => { - return this.CurrentTemperature!; + return this.TemperatureSensor!.CurrentTemperature; }); - } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Temperature Sensor Service Not Added`); } - // Humidity Sensor Service - if (device.meter?.hide_humidity) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Humidity Sensor Service`); - this.humidityservice = this.accessory.getService(this.hap.Service.HumiditySensor); - accessory.removeService(this.humidityservice!); - } else if (!this.humidityservice) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Humidity Sensor Service`); - const humidityservice = `${accessory.displayName} Humidity Sensor`; - (this.humidityservice = this.accessory.getService(this.hap.Service.HumiditySensor) - || this.accessory.addService(this.hap.Service.HumiditySensor)), humidityservice; - - this.humidityservice.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Humidity Sensor`); - if (!this.humidityservice.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.humidityservice.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Humidity Sensor`); + // Initialize Humidity Sensor Service + if (device.iosensor?.hide_humidity) { + if (this.HumiditySensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Humidity Sensor Service`); + this.HumiditySensor.Service = this.accessory.getService(this.hap.Service.HumiditySensor) as Service; + accessory.removeService(this.HumiditySensor.Service); } - this.humidityservice + } else { + accessory.context.HumiditySensor = accessory.context.HumiditySensor ?? {}; + this.HumiditySensor = { + Name: accessory.context.HumiditySensor.Name ?? `${accessory.displayName} Humidity Sensor`, + Service: accessory.getService(this.hap.Service.HumiditySensor) ?? this.accessory.addService(this.hap.Service.HumiditySensor) as Service, + CurrentRelativeHumidity: accessory.context.CurrentRelativeHumidity ?? 50, + }; + accessory.context.HumiditySensor = this.HumiditySensor as object; + + // Initialize Humidity Sensor Characteristics + this.HumiditySensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.HumiditySensor.Name) .getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity) .setProps({ minStep: 0.1, }) .onGet(() => { - return this.CurrentRelativeHumidity; + return this.HumiditySensor!.CurrentRelativeHumidity; }); - } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Humidity Sensor Service Not Added`); } - // Battery Service - const batteryService = `${accessory.displayName} Battery`; - (this.batteryService = this.accessory.getService(this.hap.Service.Battery) - || accessory.addService(this.hap.Service.Battery)), batteryService; - - this.batteryService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Battery`); - if (!this.batteryService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.batteryService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Battery`); - } - this.batteryService.setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE); + // Retrieve initial values and updateHomekit + this.refreshStatus(); // Retrieve initial values and updateHomekit this.updateHomeKitCharacteristics(); @@ -182,113 +152,77 @@ export class IOSensor { }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - if (context.scale === 'CELSIUS') { - const { temperature, humidity } = context; - const { CurrentTemperature, CurrentRelativeHumidity } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(temperature, humidity) = ' + - `Webhook:(${temperature}, ${humidity}), ` + - `current:(${CurrentTemperature}, ${CurrentRelativeHumidity})`); - this.CurrentRelativeHumidity = humidity; - this.CurrentTemperature = temperature; - this.updateHomeKitCharacteristics(); - } - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } - } - - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } + this.registerWebhook(accessory, device); } - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // Battery - this.BatteryLevel = Number(this.BLE_BatteryLevel); - if (this.BatteryLevel < 15) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(serviceData.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}, StatusLowBattery: ${this.StatusLowBattery}`); + this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}, StatusLowBattery: ${this.Battery.StatusLowBattery}`); // Humidity - if (!this.device.meter?.hide_humidity) { - this.CurrentRelativeHumidity = this.BLE_CurrentRelativeHumidity!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.CurrentRelativeHumidity}%`); + if (!this.device.iosensor?.hide_humidity) { + this.HumiditySensor!.CurrentRelativeHumidity = serviceData.humidity!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.HumiditySensor!.CurrentRelativeHumidity}%`); } // Current Temperature - if (!this.device.meter?.hide_temperature) { - this.BLE_Celsius < 0 ? 0 : this.BLE_Celsius > 100 ? 100 : this.BLE_Celsius; - this.CurrentTemperature = this.BLE_Celsius; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.CurrentTemperature}°c`); + if (!this.device.iosensor?.hide_temperature) { + serviceData.temperature!.c < 0 ? 0 : serviceData.temperature!.c > 100 ? 100 : serviceData.temperature!.c; + this.TemperatureSensor!.CurrentTemperature = serviceData.temperature!.c; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.TemperatureSensor!.CurrentTemperature}°c`); } } - async openAPIparseStatus(): Promise { + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); - // Battery - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 15) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + + // BatteryLevel + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}, StatusLowBattery: ${this.StatusLowBattery}`); + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; + } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); // Current Relative Humidity - if (!this.device.meter?.hide_humidity) { - this.CurrentRelativeHumidity = this.OpenAPI_CurrentRelativeHumidity!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.CurrentRelativeHumidity}%`); + if (!this.device.iosensor?.hide_humidity) { + this.HumiditySensor!.CurrentRelativeHumidity = deviceStatus.body.humidity!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.HumiditySensor!.CurrentRelativeHumidity}%`); } // Current Temperature - if (!this.device.meter?.hide_temperature) { - this.CurrentTemperature = this.OpenAPI_CurrentTemperature!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.CurrentTemperature}°c`); + if (!this.device.iosensor?.hide_temperature) { + this.TemperatureSensor!.CurrentTemperature = deviceStatus.body.temperature!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.TemperatureSensor!.CurrentTemperature}°c`); + } + + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); } - - // BatteryLevel - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; - } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - } - if (Number.isNaN(this.BatteryLevel)) { - this.BatteryLevel = 100; - } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` - + ` StatusLowBattery: ${this.StatusLowBattery}`); - - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; } /** @@ -303,10 +237,8 @@ export class IOSensor { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -323,107 +255,43 @@ export class IOSensor { // Start to monitor advertisement packets (async () => { // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'w', - id: this.device.bleMac, - }); + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === 'w') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_CurrentTemperature = ad.serviceData.temperature; - this.BLE_Celsius = ad.serviceData.temperature!.c; - this.BLE_Fahrenheit = ad.serviceData.temperature!.f; - this.BLE_BatteryLevel = ad.serviceData.battery; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: 'w', - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - if (ad.serviceData.humidity! > 0) { - // reject unreliable data - this.BLE_CurrentRelativeHumidity = ad.serviceData.humidity; - } - this.BLE_CurrentTemperature = ad.serviceData.temperature; - this.BLE_Celsius = ad.serviceData.temperature!.c; - this.BLE_Fahrenheit = ad.serviceData.temperature!.f; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} model: ${ad.serviceData.model}, modelName: ${ad.serviceData.modelName}, ` + - `temperature: ${JSON.stringify(ad.serviceData.temperature?.c)}, humidity: ${ad.serviceData.humidity}, ` + - `battery: ${ad.serviceData.battery}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_CurrentRelativeHumidity = deviceStatus.body.humidity!; - this.OpenAPI_CurrentTemperature = deviceStatus.body.temperature!; - this.OpenAPI_BatteryLevel = deviceStatus.body.battery; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -431,10 +299,40 @@ export class IOSensor { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { temperature, humidity } = context; + const { CurrentTemperature } = this.TemperatureSensor || { CurrentTemperature: undefined }; + const { CurrentRelativeHumidity } = this.HumiditySensor || { CurrentRelativeHumidity: undefined }; + if (context.scale !== 'CELCIUS' && device.iosensor?.convertUnitTo === undefined) { + this.warnLog(`${device.deviceType}: ${accessory.displayName} received a non-CELCIUS Webhook scale: ` + + `${context.scale}, Use the *convertUnitsTo* config under Indoor/Outdoor Sensor settings, if displaying incorrectly in HomeKit.`); + } + this.debugLog(`${device.deviceType}: ${accessory.displayName} (scale, temperature, humidity) = Webhook:(${context.scale},` + + ` ${convertUnits(temperature, context.scale, device.iosensor?.convertUnitTo)}, ${humidity}), current:(${CurrentTemperature}, ` + + `${CurrentRelativeHumidity})`); + if (device.iosensor?.hide_humidity) { + this.HumiditySensor!.CurrentRelativeHumidity = humidity; + } + if (device.iosensor?.hide_temperature) { + this.TemperatureSensor!.CurrentTemperature = convertUnits(temperature, context.scale, device.iosensor?.convertUnitTo); + } + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -444,63 +342,73 @@ export class IOSensor { async updateHomeKitCharacteristics(): Promise { const mqttmessage: string[] = []; const entry = { time: Math.round(new Date().valueOf() / 1000) }; - if (!this.device.meter?.hide_humidity) { - if (this.CurrentRelativeHumidity === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + + // CurrentRelativeHumidity + if (!this.device.iosensor?.hide_humidity) { + if (this.HumiditySensor!.CurrentRelativeHumidity === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}`); } else { - this.accessory.context.CurrentRelativeHumidity = this.CurrentRelativeHumidity; - this.humidityservice?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.CurrentRelativeHumidity); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` updateCharacteristic CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`, - ); + this.accessory.context.CurrentRelativeHumidity = this.HumiditySensor!.CurrentRelativeHumidity; + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, + this.HumiditySensor!.CurrentRelativeHumidity); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}`); if (this.device.mqttURL) { - mqttmessage.push(`"humidity": ${this.CurrentRelativeHumidity}`); + mqttmessage.push(`"humidity": ${this.HumiditySensor!.CurrentRelativeHumidity}`); } if (this.device.history) { - entry['humidity'] = this.CurrentRelativeHumidity; + entry['humidity'] = this.HumiditySensor!.CurrentRelativeHumidity; } } } - if (!this.device.meter?.hide_temperature) { - if (this.CurrentTemperature === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}`); + + // CurrentTemperature + if (!this.device.iosensor?.hide_temperature) { + if (this.TemperatureSensor!.CurrentTemperature === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); } else { + this.accessory.context.CurrentTemperature = this.TemperatureSensor!.CurrentTemperature; + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.TemperatureSensor!.CurrentTemperature); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); if (this.device.mqttURL) { - mqttmessage.push(`"temperature": ${this.CurrentTemperature}`); + mqttmessage.push(`"temperature": ${this.TemperatureSensor!.CurrentTemperature}`); } if (this.device.history) { - entry['temp'] = this.CurrentTemperature; + entry['temp'] = this.TemperatureSensor!.CurrentTemperature; } - this.accessory.context.CurrentTemperature = this.CurrentTemperature; - this.temperatureservice?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.CurrentTemperature); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.CurrentTemperature}`); } } - if (this.BatteryLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}`); + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); } else { + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); if (this.device.mqttURL) { - mqttmessage.push(`"battery": ${this.BatteryLevel}`); + mqttmessage.push(`"battery": ${this.Battery.BatteryLevel}`); } - this.accessory.context.BatteryLevel = this.BatteryLevel; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.BatteryLevel); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); } - if (this.StatusLowBattery === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); } else { + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); if (this.device.mqttURL) { - mqttmessage.push(`"lowBattery": ${this.StatusLowBattery}`); + mqttmessage.push(`"lowBattery": ${this.Battery.StatusLowBattery}`); } - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); } + + // MQTT Publish if (this.device.mqttURL) { this.mqttPublish(`{${mqttmessage.join(',')}}`); } - if (Number(this.CurrentRelativeHumidity) > 0) { + + // History Service + if (!this.device.iosensor?.hide_humidity && (Number(this.HumiditySensor!.CurrentRelativeHumidity) > 0)) { // reject unreliable data if (this.device.history) { this.historyService?.addEntry(entry); @@ -508,84 +416,6 @@ export class IOSensor { } } - /* - * Publish MQTT message for topics of - * 'homebridge-switchbot/meter/xx:xx:xx:xx:xx:xx' - */ - mqttPublish(message: any) { - const mac = this.device.deviceId - ?.toLowerCase() - .match(/[\s\S]{1,2}/g) - ?.join(':'); - const options = this.device.mqttPubOptions || {}; - this.mqttClient?.publish(`homebridge-switchbot/meter/${mac}`, `${message}`, options); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT message: ${message} options:${JSON.stringify(options)}`); - } - - /* - * Setup MQTT hadler if URL is specified. - */ - async setupMqtt(device: device & devicesConfig): Promise { - if (device.mqttURL) { - try { - const { connectAsync } = asyncmqtt; - this.mqttClient = await connectAsync(device.mqttURL, device.mqttOptions || {}); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT connection has been established successfully.`); - this.mqttClient.on('error', (e: Error) => { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to publish MQTT messages. ${e}`); - }); - } catch (e) { - this.mqttClient = null; - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to establish MQTT connection. ${e}`); - } - } - } - - /* - * Setup EVE history graph feature if enabled. - */ - async setupHistoryService(device: device & devicesConfig): Promise { - const mac = this.device - .deviceId!.match(/.{1,2}/g)! - .join(':') - .toLowerCase(); - this.historyService = device.history - ? new this.platform.fakegatoAPI('room', this.accessory, { - log: this.platform.log, - storage: 'fs', - filename: `${hostname().split('.')[0]}_${mac}_persist.json`, - }) - : null; - } - - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); - } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'i', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); - } - } - async BLERefreshConnection(switchbot: any): Promise { this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} wasn't able to establish BLE Connection, node-switchbot:` + ` ${JSON.stringify(switchbot)}`); @@ -595,249 +425,26 @@ export class IOSensor { } } - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); - } - } - - async apiError(e: any): Promise { - if (!this.device.meter?.hide_humidity) { - this.humidityservice?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); - } - if (!this.device.meter?.hide_temperature) { - this.temperatureservice?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); - } - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); - } - - async deviceContext() { - if (this.CurrentRelativeHumidity === undefined) { - this.CurrentRelativeHumidity = 0; - } else { - this.CurrentRelativeHumidity = this.accessory.context.CurrentRelativeHumidity; - } - if (this.CurrentTemperature === undefined) { - this.CurrentTemperature = 0; - } else { - this.CurrentTemperature = this.accessory.context.CurrentTemperature; - } - if (this.BatteryLevel === undefined) { - this.BatteryLevel = 100; - } else { - this.BatteryLevel = this.accessory.context.BatteryLevel; - } - if (this.StatusLowBattery === undefined) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - } else { - this.StatusLowBattery = this.accessory.context.StatusLowBattery; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.meter) { - config = device.meter; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.mqttURL !== undefined) { - config['mqttURL'] = device.mqttURL; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); + if (!this.device.iosensor?.hide_humidity) { + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, 50); } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); + if (!this.device.iosensor?.hide_temperature) { + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, 30); } + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, 100); } } - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } + async apiError(e: any): Promise { + if (!this.device.iosensor?.hide_humidity) { + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + if (!this.device.iosensor?.hide_temperature) { + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); + } + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); } } diff --git a/src/device/lightstrip.ts b/src/device/lightstrip.ts index 01dae57b..e4709146 100644 --- a/src/device/lightstrip.ts +++ b/src/device/lightstrip.ts @@ -1,141 +1,106 @@ import { request } from 'undici'; -import { sleep } from '../utils.js'; -import { interval, Subject } from 'rxjs'; -import { SwitchBotPlatform } from '../platform.js'; -import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { - Service, PlatformAccessory, CharacteristicValue, ControllerConstructor, Controller, ControllerServiceMap, API, Logging, HAP, -} from 'homebridge'; -import { device, devicesConfig, hs2rgb, rgb2hs, deviceStatus, serviceData, m2hs, Devices, SwitchBotPlatformConfig } from '../settings.js'; +import { deviceBase } from './device.js'; +import { Devices } from '../settings.js'; +import { hs2rgb, rgb2hs, m2hs } from '../utils.js'; +import { interval, skipWhile, Subject } from 'rxjs'; +import { debounceTime, take, tap } from 'rxjs/operators'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { device, devicesConfig, deviceStatus, serviceData } from '../settings.js'; +import type { Service, PlatformAccessory, CharacteristicValue, ControllerConstructor, Controller, ControllerServiceMap } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class StripLight { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class StripLight extends deviceBase { // Services - lightBulbService!: Service; - - // Characteristic Values - On!: CharacteristicValue; - Hue!: CharacteristicValue; - Saturation!: CharacteristicValue; - Brightness!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - ColorTemperature?: CharacteristicValue; - - // OpenAPI Status - OpenAPI_On: deviceStatus['power']; - OpenAPI_RGB: deviceStatus['color']; - OpenAPI_Brightness: deviceStatus['brightness']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - - // BLE Status - BLE_On: serviceData['state']; - BLE_Brightness: serviceData['brightness']; - - // BLE Others - BLE_IsConnected?: boolean; - - // Config - set_minStep?: number; - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; - adaptiveLightingShift?: number; + private LightBulb: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + Hue: CharacteristicValue; + Saturation: CharacteristicValue; + Brightness: CharacteristicValue; + ColorTemperature?: CharacteristicValue; + }; // Adaptive Lighting AdaptiveLightingController?: ControllerConstructor | Controller; - minKelvin!: number; - maxKelvin!: number; - - // Others - cacheKelvin!: number; - change!: string; + adaptiveLightingShift?: number; // Updates stripLightUpdateInProgress!: boolean; doStripLightUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); - this.deviceContext(); - this.deviceConfig(device); - + super(platform, accessory, device); + this.adaptiveLighting(device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doStripLightUpdate = new Subject(); this.stripLightUpdateInProgress = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, 'W1701100') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Lightbulb service if it exists, otherwise create a new Lightbulb service - // you can create multiple services for each accessory - const lightBulbService = `${accessory.displayName} ${device.deviceType}`; - (this.lightBulbService = accessory.getService(this.hap.Service.Lightbulb) - || accessory.addService(this.hap.Service.Lightbulb)), lightBulbService; - + // Initialize the LightBulb Service + accessory.context.LightBulb = accessory.context.LightBulb ?? {}; + this.LightBulb = { + Name: accessory.context.LightBulb.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Lightbulb) ?? accessory.addService(this.hap.Service.Lightbulb) as Service, + On: accessory.context.On ?? false, + Hue: accessory.context.Hue ?? 0, + Saturation: accessory.context.Saturation ?? 0, + Brightness: accessory.context.Brightness ?? 0, + ColorTemperature: accessory.context.ColorTemperature ?? 140, + }; + accessory.context.LightBulb = this.LightBulb as object; + + // Adaptive Lighting if (this.adaptiveLightingShift === -1 && this.accessory.context.adaptiveLighting) { - this.accessory.removeService(this.lightBulbService); - this.lightBulbService = this.accessory.addService(this.hap.Service.Lightbulb); + this.accessory.removeService(this.LightBulb.Service); + this.LightBulb.Service = this.accessory.addService(this.hap.Service.Lightbulb); this.accessory.context.adaptiveLighting = false; this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} adaptiveLighting: ${this.accessory.context.adaptiveLighting}`); } - - this.lightBulbService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.lightBulbService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.lightBulbService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); + if (this.adaptiveLightingShift !== -1) { + this.AdaptiveLightingController = new platform.api.hap.AdaptiveLightingController(this.LightBulb.Service, { + customTemperatureAdjustment: this.adaptiveLightingShift, + }); + this.accessory.configureController(this.AdaptiveLightingController); + this.accessory.context.adaptiveLighting = true; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} adaptiveLighting: ${this.accessory.context.adaptiveLighting},` + + ` adaptiveLightingShift: ${this.adaptiveLightingShift}`); } - // handle on / off events using the On characteristic - this.lightBulbService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} adaptiveLightingShift: ${this.adaptiveLightingShift}`); + + // Initialize LightBulb Characteristics + this.LightBulb.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LightBulb.Name) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.LightBulb.On; + }) + .onSet(this.OnSet.bind(this)); - // handle Brightness events using the Brightness characteristic - this.lightBulbService + // Initialize LightBulb Brightness Characteristic + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.Brightness) .setProps({ - minStep: this.minStep(device), + minStep: device.striplight?.set_minStep ?? 1, minValue: 0, maxValue: 100, validValueRanges: [0, 100], }) .onGet(() => { - return this.Brightness; + return this.LightBulb.Brightness; }) .onSet(this.BrightnessSet.bind(this)); - // handle ColorTemperature events using the ColorTemperature characteristic - this.lightBulbService + // Initialize LightBulb ColorTemperature Characteristic + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.ColorTemperature) .setProps({ minValue: 140, @@ -143,12 +108,12 @@ export class StripLight { validValueRanges: [140, 500], }) .onGet(() => { - return this.ColorTemperature!; + return this.LightBulb.ColorTemperature!; }) .onSet(this.ColorTemperatureSet.bind(this)); - // handle Hue events using the Hue characteristic - this.lightBulbService + // Initialize LightBulb Hue Characteristic + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.Hue) .setProps({ minValue: 0, @@ -156,12 +121,12 @@ export class StripLight { validValueRanges: [0, 360], }) .onGet(() => { - return this.Hue; + return this.LightBulb.Hue; }) .onSet(this.HueSet.bind(this)); - // handle Hue events using the Hue characteristic - this.lightBulbService + // Initialize LightBulb Saturation Characteristic + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.Saturation) .setProps({ minValue: 0, @@ -169,22 +134,12 @@ export class StripLight { validValueRanges: [0, 100], }) .onGet(() => { - return this.Saturation; + return this.LightBulb.Saturation; }) .onSet(this.SaturationSet.bind(this)); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} adaptiveLightingShift: ${this.adaptiveLightingShift}`); - if (this.adaptiveLightingShift !== -1) { - this.AdaptiveLightingController = new platform.api.hap.AdaptiveLightingController(this.lightBulbService, { - customTemperatureAdjustment: this.adaptiveLightingShift, - }); - this.accessory.configureController(this.AdaptiveLightingController); - this.accessory.context.adaptiveLighting = true; - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} adaptiveLighting: ${this.accessory.context.adaptiveLighting},` + - ` adaptiveLightingShift: ${this.adaptiveLightingShift}`, - ); - } + // Retrieve initial values and updateHomekit + this.refreshStatus(); // Update Homekit this.updateHomeKitCharacteristics(); @@ -197,46 +152,7 @@ export class StripLight { }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - const { powerState, brightness, color, colorTemperature } = context; - const { On, Brightness, Hue, Saturation, ColorTemperature } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(powerState, brightness, color, colorTemperature) = ' + - `Webhook:(${powerState}, ${brightness}, ${color}, ${colorTemperature}), ` + - `current:(${On}, ${Brightness}, ${Hue}, ${Saturation}, ${ColorTemperature})`); - this.On = powerState === 'ON' ? true : false; - this.Brightness = brightness; - - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} color: ${JSON.stringify(color)}`); - const [red, green, blue] = color!.split(':'); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} red: ${JSON.stringify(red)}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} green: ${JSON.stringify(green)}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} blue: ${JSON.stringify(blue)}`); - - const [hue, saturation] = rgb2hs(Number(red), Number(green), Number(blue)); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + ` hs: ${JSON.stringify(rgb2hs(Number(red), Number(green), Number(blue)))}`, - ); - - // Hue - this.Hue = hue; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.Hue}`); - - // Saturation - this.Saturation = saturation; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.Saturation}`); - - this.updateHomeKitCharacteristics(); - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); // Watch for Bulb change events // We put in a debounce of 1000ms so we don't make duplicate calls @@ -245,93 +161,114 @@ export class StripLight { tap(() => { this.stripLightUpdateInProgress = true; }), - debounceTime(this.platform.config.options!.pushRate! * 1000), + debounceTime(this.devicePushRate * 1000), ) .subscribe(async () => { try { await this.pushChanges(); } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + - ` Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + + ` Error Message: ${JSON.stringify(e.message)}`); } this.stripLightUpdateInProgress = false; }); } - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // State - switch (this.BLE_On) { + switch (serviceData.state) { case 'on': - this.On = true; + this.LightBulb.On = true; break; default: - this.On = false; + this.LightBulb.On = false; + } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); + + // Brightness + this.LightBulb.Brightness = Number(serviceData.brightness); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); + + // Color, Hue & Brightness + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} red: ${serviceData.red}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} green: ${serviceData.green}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} blue: ${serviceData.blue}`); + + const [hue, saturation] = rgb2hs(Number(serviceData.red), Number(serviceData.green), Number(serviceData.blue)); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` hs: ${JSON.stringify(rgb2hs(Number(serviceData.red), Number(serviceData.green), Number(serviceData.blue)))}`); + + // Hue + this.LightBulb.Hue = hue; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.LightBulb.Hue}`); + + // Saturation + this.LightBulb.Saturation = saturation; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); + + // ColorTemperature + if (serviceData.color_temperature) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${serviceData.color_temperature}`); + this.LightBulb.ColorTemperature = serviceData.color_temperature!; + + this.LightBulb.ColorTemperature = Math.max(Math.min(this.LightBulb.ColorTemperature, 500), 140); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); } - async openAPIparseStatus(): Promise { + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); - switch (this.OpenAPI_On) { + switch (deviceStatus.body.power) { case 'on': - this.On = true; + this.LightBulb.On = true; break; default: - this.On = false; + this.LightBulb.On = false; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); // Brightness - this.Brightness = Number(this.OpenAPI_Brightness); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.Brightness}`); + this.LightBulb.Brightness = Number(deviceStatus.body.brightness); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); // Color, Hue & Brightness - if (this.OpenAPI_RGB) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} color: ${JSON.stringify(this.OpenAPI_RGB)}`); - const [red, green, blue] = this.OpenAPI_RGB!.split(':'); + if (deviceStatus.body.color) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} color: ${JSON.stringify(deviceStatus.body.color)}`); + const [red, green, blue] = deviceStatus.body.color!.split(':'); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} red: ${JSON.stringify(red)}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} green: ${JSON.stringify(green)}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} blue: ${JSON.stringify(blue)}`); const [hue, saturation] = rgb2hs(Number(red), Number(green), Number(blue)); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + ` hs: ${JSON.stringify(rgb2hs(Number(red), Number(green), Number(blue)))}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` hs: ${JSON.stringify(rgb2hs(Number(red), Number(green), Number(blue)))}`); // Hue - this.Hue = hue; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.Hue}`); + this.LightBulb.Hue = hue; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.LightBulb.Hue}`); // Saturation - this.Saturation = saturation; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.Saturation}`); + this.LightBulb.Saturation = saturation; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); } - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); + } } /** @@ -346,10 +283,8 @@ export class StripLight { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -366,96 +301,43 @@ export class StripLight { // Start to monitor advertisement packets (async () => { // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'r', - id: this.device.bleMac, - }); + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === 'r') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: 'r', - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} state: ${ad.serviceData.state}, ` + - `delay: ${ad.serviceData.delay}, timer: ${ad.serviceData.timer}, syncUtcTime: ${ad.serviceData.syncUtcTime} ` + - `wifiRssi: ${ad.serviceData.wifiRssi}, overload: ${ad.serviceData.overload}, currentPower: ${ad.serviceData.currentPower}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_On = deviceStatus.body.power; - this.OpenAPI_RGB = deviceStatus.body.color; - this.OpenAPI_Brightness = deviceStatus.body.brightness; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -463,10 +345,79 @@ export class StripLight { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { powerState, brightness, color, colorTemperature } = context; + const { On, Brightness, Hue, Saturation, ColorTemperature } = this.LightBulb; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (powerState, brightness, color, colorTemperature) = Webhook: (${powerState},` + + ` ${brightness}, ${color}, ${colorTemperature}), current:(${On}, ${Brightness}, ${Hue}, ${Saturation}, ${ColorTemperature})`); + + // On + this.LightBulb.On = powerState === 'ON' ? true : false; + if (accessory.context.Brightness !== this.LightBulb.On) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} On: ${this.LightBulb.On}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} On: ${this.LightBulb.On}`); + } + + // Brightness + this.LightBulb.Brightness = brightness; + if (accessory.context.Brightness !== this.LightBulb.Brightness) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); + } + + this.debugLog(`${device.deviceType}: ${accessory.displayName} color: ${JSON.stringify(color)}`); + const [red, green, blue] = color!.split(':'); + this.debugLog(`${device.deviceType}: ${accessory.displayName} red: ${JSON.stringify(red)}`); + this.debugLog(`${device.deviceType}: ${accessory.displayName} green: ${JSON.stringify(green)}`); + this.debugLog(`${device.deviceType}: ${accessory.displayName} blue: ${JSON.stringify(blue)}`); + + const [hue, saturation] = rgb2hs(Number(red), Number(green), Number(blue)); + this.debugLog( + `${device.deviceType}: ${accessory.displayName}` + + ` hs: ${JSON.stringify(rgb2hs(Number(red), Number(green), Number(blue)))}`); + + // Hue + this.LightBulb.Hue = hue; + if (accessory.context.Hue !== this.LightBulb.Hue) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} Hue: ${this.LightBulb.Hue}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} Hue: ${this.LightBulb.Hue}`); + } + + // Saturation + this.LightBulb.Saturation = saturation; + if (accessory.context.Saturation !== this.LightBulb.Saturation) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); + } + + // ColorTemperature + this.LightBulb.ColorTemperature = colorTemperature; + if (accessory.context.ColorTemperature !== this.LightBulb.ColorTemperature) { + this.infoLog(`${device.deviceType}: ${accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); + } + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -489,9 +440,8 @@ export class StripLight { await this.openAPIpushChanges(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, pushChanges will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, pushChanges will not happen.`); } // Refresh the status from the API interval(15000) @@ -504,8 +454,9 @@ export class StripLight { async BLEpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`); - if (this.On !== this.accessory.context.On) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges On: ${this.On} OnCached: ${this.accessory.context.On}`); + if (this.LightBulb.On !== this.accessory.context.On) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges` + + ` On: ${this.LightBulb.On} OnCached: ${this.accessory.context.On}`); const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -519,11 +470,11 @@ export class StripLight { id: this.device.bleMac, }) .then(async (device_list: any) => { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); - return await this.retry({ - max: this.maxRetry(), + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); + return await this.retryBLE({ + max: await this.maxRetryBLE(), fn: async () => { - if (this.On) { + if (this.LightBulb.On) { return await device_list[0].turnOn({ id: this.device.bleMac }); } else { return await device_list[0].turnOff({ id: this.device.bleMac }); @@ -533,34 +484,40 @@ export class StripLight { }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.On = false; + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `On: ${this.LightBulb.On} sent over BLE, sent successfully`); + this.LightBulb.On = false; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); // Push Brightness Update - if (this.On) { + if (this.LightBulb.On) { await this.BLEpushBrightnessChanges(); } // Push Hue & Saturation Update - if (this.On) { + if (this.LightBulb.On) { await this.BLEpushRGBChanges(); } + if (this.LightBulb.ColorTemperature !== this.accessory.context.ColorTemperature) { + const kelvin = Math.round(1000000 / Number(this.LightBulb.ColorTemperature)); + this.accessory.context.kelvin = kelvin; + } else { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushColorTemperatureChanges.` + + `ColorTemperature: ${this.LightBulb.ColorTemperature}, ColorTemperatureCached: ${this.accessory.context.ColorTemperature}`); + } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges.` + `On: ${this.On}, ` + `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges,` + + ` On: ${this.LightBulb.On}, OnCached: ${this.accessory.context.On}`); } } async BLEpushBrightnessChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushBrightnessChanges`); - if (this.Brightness !== this.accessory.context.Brightness) { + if (this.LightBulb.Brightness !== this.accessory.context.Brightness) { const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -574,37 +531,33 @@ export class StripLight { id: this.device.bleMac, }) .then(async (device_list: any) => { - this.infoLog(`${this.accessory.displayName} Target Brightness: ${this.Brightness}`); - return await device_list[0].setBrightness(this.Brightness); + this.infoLog(`${this.accessory.displayName} Target Brightness: ${this.LightBulb.Brightness}`); + return await device_list[0].setBrightness(this.LightBulb.Brightness); }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.On = false; + this.LightBulb.On = false; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushBrightnessChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushBrightnessChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushBrightnessChanges.` + - `Brightness: ${this.Brightness}, ` + - `BrightnessCached: ${this.accessory.context.Brightness}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushBrightnessChanges.` + + `Brightness: ${this.LightBulb.Brightness}, ` + + `BrightnessCached: ${this.accessory.context.Brightness}`); } } async BLEpushRGBChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushRGBChanges`); - if (this.Hue !== this.accessory.context.Hue || this.Saturation !== this.accessory.context.Saturation) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${JSON.stringify(this.Hue)}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${JSON.stringify(this.Saturation)}`); + if (this.LightBulb.Hue !== this.accessory.context.Hue || this.LightBulb.Saturation !== this.accessory.context.Saturation) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${JSON.stringify(this.LightBulb.Hue)}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${JSON.stringify(this.LightBulb.Saturation)}`); - const [red, green, blue] = hs2rgb(Number(this.Hue), Number(this.Saturation)); + const [red, green, blue] = hs2rgb(Number(this.LightBulb.Hue), Number(this.LightBulb.Saturation)); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} rgb: ${JSON.stringify([red, green, blue])}`); const switchbot = await this.platform.connectBLE(); @@ -620,34 +573,30 @@ export class StripLight { id: this.device.bleMac, }) .then(async (device_list: any) => { - this.infoLog(`${this.accessory.displayName} Target RGB: ${(this.Brightness, red, green, blue)}`); - return await device_list[0].setRGB(this.Brightness, red, green, blue); + this.infoLog(`${this.accessory.displayName} Target RGB: ${(this.LightBulb.Brightness, red, green, blue)}`); + return await device_list[0].setRGB(this.LightBulb.Brightness, red, green, blue); }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.On = false; + this.LightBulb.On = false; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushRGBChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushRGBChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushRGBChanges. Hue: ${this.Hue}, ` + - `HueCached: ${this.accessory.context.Hue}, Saturation: ${this.Saturation}, SaturationCached: ${this.accessory.context.Saturation}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushRGBChanges. Hue: ${this.LightBulb.Hue}, HueCached: ` + + `${this.accessory.context.Hue}, Saturation: ${this.LightBulb.Saturation}, SaturationCached: ${this.accessory.context.Saturation}`); } } async openAPIpushChanges() { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`); - if (this.On !== this.accessory.context.On) { - const command = this.On ? 'turnOn' : 'turnOff'; - /*if (this.On) { + if (this.LightBulb.On !== this.accessory.context.On) { + const command = this.LightBulb.On ? 'turnOn' : 'turnOff'; + /*if (this.LightBulb.On) { command = 'turnOn'; } else { command = 'turnOff'; @@ -672,40 +621,38 @@ export class StripLight { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.` + - `On: ${this.On}, ` + - `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.` + + `On: ${this.LightBulb.On}, ` + + `OnCached: ${this.accessory.context.On}`); } // Push Hue & Saturation Update - if (this.On) { + if (this.LightBulb.On) { await this.pushHueSaturationChanges(); } // Push Brightness Update - if (this.On) { + if (this.LightBulb.On) { await this.pushBrightnessChanges(); } } async pushHueSaturationChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushHueSaturationChanges`); - if (this.Hue !== this.accessory.context.Hue || this.Saturation !== this.accessory.context.Saturation) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${JSON.stringify(this.Hue)}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${JSON.stringify(this.Saturation)}`); - const [red, green, blue] = hs2rgb(Number(this.Hue), Number(this.Saturation)); + if (this.LightBulb.Hue !== this.accessory.context.Hue || this.LightBulb.Saturation !== this.accessory.context.Saturation) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${JSON.stringify(this.LightBulb.Hue)}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${JSON.stringify(this.LightBulb.Saturation)}`); + const [red, green, blue] = hs2rgb(Number(this.LightBulb.Hue), Number(this.LightBulb.Saturation)); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} rgb: ${JSON.stringify([red, green, blue])}`); // Make Push On request to the API const bodyChange = JSON.stringify({ @@ -728,31 +675,29 @@ export class StripLight { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushHueSaturationChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushHueSaturationChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No pushHueSaturationChanges. Hue: ${this.Hue}, ` + - `HueCached: ${this.accessory.context.Hue}, Saturation: ${this.Saturation}, SaturationCached: ${this.accessory.context.Saturation}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushHueSaturationChanges. Hue: ${this.LightBulb.Hue}, HueCached: ` + + `${this.accessory.context.Hue}, Saturation: ${this.LightBulb.Saturation}, SaturationCached: ${this.accessory.context.Saturation}`); } } async pushBrightnessChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} pushBrightnessChanges`); - if (this.Brightness !== this.accessory.context.Brightness) { + if (this.LightBulb.Brightness !== this.accessory.context.Brightness) { const bodyChange = JSON.stringify({ command: 'setBrightness', - parameter: `${this.Brightness}`, + parameter: `${this.LightBulb.Brightness}`, commandType: 'command', }); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`); @@ -770,23 +715,21 @@ export class StripLight { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushBrightnessChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushBrightnessChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No pushBrightnessChanges,` + - `Brightness: ${this.Brightness}, ` + - `BrightnessCached: ${this.accessory.context.Brightness}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No pushBrightnessChanges,` + + `Brightness: ${this.LightBulb.Brightness}, ` + + `BrightnessCached: ${this.accessory.context.Brightness}`); } } @@ -794,13 +737,13 @@ export class StripLight { * Handle requests to set the value of the "On" characteristic */ async OnSet(value: CharacteristicValue): Promise { - if (this.On === this.accessory.context.On) { + if (this.LightBulb.On === this.accessory.context.On) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set On: ${value}`); } else { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set On: ${value}`); } - this.On = value; + this.LightBulb.On = value; this.doStripLightUpdate.next(); } @@ -808,15 +751,15 @@ export class StripLight { * Handle requests to set the value of the "Brightness" characteristic */ async BrightnessSet(value: CharacteristicValue): Promise { - if (this.Brightness === this.accessory.context.Brightness) { + if (this.LightBulb.Brightness === this.accessory.context.Brightness) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Brightness: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`); } - this.Brightness = value; + this.LightBulb.Brightness = value; this.doStripLightUpdate.next(); } @@ -824,30 +767,31 @@ export class StripLight { * Handle requests to set the value of the "ColorTemperature" characteristic */ async ColorTemperatureSet(value: CharacteristicValue): Promise { - if (this.ColorTemperature === this.accessory.context.ColorTemperature) { + if (this.LightBulb.ColorTemperature === this.accessory.context.ColorTemperature) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set ColorTemperature: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set ColorTemperature: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set ColorTemperature: ${value}`); } + const minKelvin = 2000; + const maxKelvin = 9000; // Convert mired to kelvin to nearest 100 (SwitchBot seems to need this) const kelvin = Math.round(1000000 / Number(value) / 100) * 100; - // Check and increase/decrease kelvin to range of device - const k = Math.min(Math.max(kelvin, this.minKelvin), this.maxKelvin); + const k = Math.min(Math.max(kelvin, minKelvin), maxKelvin); - if (!this.accessory.context.On || this.cacheKelvin === k) { + if (!this.accessory.context.On || this.accessory.context.maxKelvin === k) { return; } // Updating the hue/sat to the corresponding values mimics native adaptive lighting const hs = m2hs(value); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Hue, hs[0]); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Saturation, hs[1]); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Hue, hs[0]); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Saturation, hs[1]); - this.ColorTemperature = value; + this.LightBulb.ColorTemperature = value; this.doStripLightUpdate.next(); } @@ -855,17 +799,17 @@ export class StripLight { * Handle requests to set the value of the "Hue" characteristic */ async HueSet(value: CharacteristicValue): Promise { - if (this.Hue === this.accessory.context.Hue) { + if (this.LightBulb.Hue === this.accessory.context.Hue) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Hue: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Hue: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Hue: ${value}`); } - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); - this.Hue = value; + this.LightBulb.Hue = value; this.doStripLightUpdate.next(); } @@ -873,89 +817,61 @@ export class StripLight { * Handle requests to set the value of the "Saturation" characteristic */ async SaturationSet(value: CharacteristicValue): Promise { - if (this.Saturation === this.accessory.context.Saturation) { + if (this.LightBulb.Saturation === this.accessory.context.Saturation) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Saturation: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Saturation: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Saturation: ${value}`); } - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, 140); - this.Saturation = value; + this.LightBulb.Saturation = value; this.doStripLightUpdate.next(); } async updateHomeKitCharacteristics(): Promise { // On - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.LightBulb.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); } else { - this.accessory.context.On = this.On; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + this.accessory.context.On = this.LightBulb.On; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.On, this.LightBulb.On); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.LightBulb.On}`); } // Brightness - if (this.Brightness === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.Brightness}`); + if (this.LightBulb.Brightness === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); } else { - this.accessory.context.Brightness = this.Brightness; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Brightness, this.Brightness); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Brightness: ${this.Brightness}`); + this.accessory.context.Brightness = this.LightBulb.Brightness; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Brightness, this.LightBulb.Brightness); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Brightness: ${this.LightBulb.Brightness}`); } // ColorTemperature - if (this.ColorTemperature === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.ColorTemperature}`); + if (this.LightBulb.ColorTemperature === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ColorTemperature: ${this.LightBulb.ColorTemperature}`); } else { - this.accessory.context.ColorTemperature = this.ColorTemperature; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.ColorTemperature, this.ColorTemperature); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic ColorTemperature: ${this.ColorTemperature}`); + this.accessory.context.ColorTemperature = this.LightBulb.ColorTemperature; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, this.LightBulb.ColorTemperature); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` ColorTemperature: ${this.LightBulb.ColorTemperature}`); } // Hue - if (this.Hue === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.Hue}`); + if (this.LightBulb.Hue === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Hue: ${this.LightBulb.Hue}`); } else { - this.accessory.context.Hue = this.Hue; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Hue, this.Hue); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Hue: ${this.Hue}`); + this.accessory.context.Hue = this.LightBulb.Hue; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Hue, this.LightBulb.Hue); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Hue: ${this.LightBulb.Hue}`); } // Saturation - if (this.Saturation === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.Saturation}`); + if (this.LightBulb.Saturation === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Saturation: ${this.LightBulb.Saturation}`); } else { - this.accessory.context.Saturation = this.Saturation; - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Saturation, this.Saturation); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Saturation: ${this.Saturation}`); - } - } - - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); - } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} customBLEaddress: ${this.device.customBLEaddress}`); - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'r', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); + this.accessory.context.Saturation = this.LightBulb.Saturation; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Saturation, this.LightBulb.Saturation); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Saturation: ${this.LightBulb.Saturation}`); } } @@ -975,35 +891,6 @@ export class StripLight { } } - async retry({ max, fn }: { max: number; fn: { (): any; (): Promise } }): Promise { - return fn().catch(async (e: any) => { - if (max === 0) { - throw e; - } - this.infoLog(e); - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Retrying`); - await sleep(1000); - return this.retry({ max: max - 1, fn }); - }); - } - - maxRetry(): number { - if (this.device.maxRetry) { - return this.device.maxRetry; - } else { - return 5; - } - } - - minStep(device: device & devicesConfig): number { - if (device.striplight?.set_minStep) { - this.set_minStep = device.striplight?.set_minStep; - } else { - this.set_minStep = 1; - } - return this.set_minStep; - } - async adaptiveLighting(device: device & devicesConfig): Promise { if (device.striplight?.adaptiveLightingShift) { this.adaptiveLightingShift = device.striplight.adaptiveLightingShift; @@ -1014,254 +901,17 @@ export class StripLight { } } - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); - } - } - - apiError(e: any): void { - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.On, e); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Hue, e); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Brightness, e); - this.lightBulbService.updateCharacteristic(this.hap.Characteristic.Saturation, e); - } - - async deviceContext() { - if (this.On === undefined) { - this.On = false; - } else { - this.On = this.accessory.context.On; - } - if (this.Hue === undefined) { - this.Hue = 0; - } else { - this.Hue = this.accessory.context.Hue; - } - if (this.Brightness === undefined) { - this.Brightness = 0; - } else { - this.Brightness = this.accessory.context.Brightness; - } - if (this.Saturation === undefined) { - this.Saturation = 0; - } else { - this.Saturation = this.accessory.context.Saturation; - } - if (this.ColorTemperature === undefined) { - this.ColorTemperature = 140; - } else { - this.ColorTemperature = this.accessory.context.ColorTemperature; - } - this.minKelvin = 2000; - this.maxKelvin = 9000; - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.striplight) { - config = device.striplight; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.offline !== undefined) { - config['offline'] = device.offline; - } - if (device.maxRetry !== undefined) { - config['maxRetry'] = device.maxRetry; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.On, false); } } - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + async apiError(e: any): Promise { + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.On, e); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Hue, e); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Brightness, e); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Saturation, e); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.ColorTemperature, e); } } diff --git a/src/device/lock.ts b/src/device/lock.ts index 2dc1244a..49a75d2c 100644 --- a/src/device/lock.ts +++ b/src/device/lock.ts @@ -1,174 +1,153 @@ import { request } from 'undici'; -import { sleep } from '../utils.js'; +import { deviceBase } from './device.js'; import { interval, Subject } from 'rxjs'; -import { SwitchBotPlatform } from '../platform.js'; +import { Devices } from '../settings.js'; import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { Service, PlatformAccessory, CharacteristicValue, API, Logging, HAP } from 'homebridge'; -import { device, devicesConfig, deviceStatus, Devices, serviceData, SwitchBotPlatformConfig } from '../settings.js'; - -export class Lock { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; +import type { device, devicesConfig, deviceStatus, serviceData } from '../settings.js'; + +export class Lock extends deviceBase { // Services - lockService: Service; - batteryService: Service; - contactSensorService?: Service; - latchButtonService?: Service; - - // Characteristic Values - BatteryLevel!: CharacteristicValue; - LockTargetState!: CharacteristicValue; - LockCurrentState!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - StatusLowBattery!: CharacteristicValue; - ContactSensorState!: CharacteristicValue; - - // OpenAPI Status - OpenAPI_BatteryLevel: deviceStatus['battery']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_LockCurrentState!: deviceStatus['lockState']; - OpenAPI_ContactSensorState!: deviceStatus['doorState']; - - // BLE Status - BLE_BatteryLevel: serviceData['battery']; - BLE_LockCurrentState: serviceData['state']; - BLE_Calibration: serviceData['calibration']; - BLE_ContactSensorState: serviceData['door_open']; - - // BLE Others - BLE_IsConnected?: boolean; - - // Config - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; + private LockMechanism: { + Name: CharacteristicValue; + Service: Service; + LockTargetState: CharacteristicValue; + LockCurrentState: CharacteristicValue; + }; + + private Battery: { + Name: CharacteristicValue; + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + }; + + private ContactSensor?: { + Name: CharacteristicValue; + Service: Service; + ContactSensorState: CharacteristicValue; + }; + + private Switch?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; // Updates lockUpdateInProgress!: boolean; doLockUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); - this.deviceContext(); - this.deviceConfig(device); - + super(platform, accessory, device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doLockUpdate = new Subject(); this.lockUpdateInProgress = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, 'W1601700') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the LockMechanism service if it exists, otherwise create a new LockMechanism service - // you can create multiple services for each accessory - const lockService = `${accessory.displayName} ${device.deviceType}`; - (this.lockService = accessory.getService(this.hap.Service.LockMechanism) - || accessory.addService(this.hap.Service.LockMechanism)), lockService; - - this.lockService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.lockService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.lockService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/LockMechanism - - - // create handlers for required characteristics - this.lockService.getCharacteristic(this.hap.Characteristic.LockTargetState).onSet(this.LockTargetStateSet.bind(this)); - - - // Latch Button Service - if (device.lock?.activate_latchbutton === false) { // remove the service when this variable is false - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Latch Button Service`); - this.latchButtonService = accessory.getService(this.hap.Service.Switch); - if (this.latchButtonService) { - accessory.removeService(this.latchButtonService); - this.latchButtonService = undefined; // Reset the service variable to undefined - } - } else - if (!this.latchButtonService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Adding Latch Button Service`); - const latchServiceName = `${accessory.displayName} Latch`; - this.latchButtonService = accessory.getService(this.hap.Service.Switch) - || accessory.addService(this.hap.Service.Switch, latchServiceName, 'LatchButtonServiceIdentifier'); - - this.latchButtonService.setCharacteristic(this.hap.Characteristic.Name, latchServiceName); - - if (!this.latchButtonService.testCharacteristic(this.hap.Characteristic.On)) { - this.latchButtonService.addCharacteristic(this.hap.Characteristic.On); - } - - this.latchButtonService.getCharacteristic(this.hap.Characteristic.On) - .on('set', (value, callback) => { - if (typeof value === 'boolean') { - this.handleLatchCharacteristic(value, callback); - } else { - callback(new Error('Wrong characteristic value type')); - } - }); - } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Latch Button Service already exists`); - } + // Initialize LockMechanism Service + accessory.context.LockMechanism = accessory.context.LockMechanism ?? {}; + this.LockMechanism = { + Name: accessory.context.LockMechanism.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.LockMechanism) ?? accessory.addService(this.hap.Service.LockMechanism) as Service, + LockTargetState: accessory.context.LockTargetState ?? this.hap.Characteristic.LockTargetState.SECURED, + LockCurrentState: accessory.context.LockCurrentState ?? this.hap.Characteristic.LockCurrentState.SECURED, + }; + accessory.context.LockMechanism = this.LockMechanism as object; + + // Initialize LockMechanism Characteristics + this.LockMechanism.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LockMechanism.Name) + .getCharacteristic(this.hap.Characteristic.LockTargetState) + .onGet(() => { + return this.LockMechanism.LockTargetState; + }) + .onSet(this.LockTargetStateSet.bind(this)); + + // Initialize Battery property + accessory.context.Battery = accessory.context.Battery ?? {}; + this.Battery = { + Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Characteristics + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE) + .getCharacteristic(this.hap.Characteristic.BatteryLevel) + .onGet(() => { + return this.Battery.BatteryLevel; + }); + this.Battery.Service + .getCharacteristic(this.hap.Characteristic.StatusLowBattery) + .onGet(() => { + return this.Battery.StatusLowBattery; + }); // Contact Sensor Service if (device.lock?.hide_contactsensor) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Contact Sensor Service`); - this.contactSensorService = this.accessory.getService(this.hap.Service.ContactSensor); - accessory.removeService(this.contactSensorService!); - } else if (!this.contactSensorService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Contact Sensor Service`); - const contactSensorService = `${accessory.displayName} Contact Sensor`; - (this.contactSensorService = this.accessory.getService(this.hap.Service.ContactSensor) - || this.accessory.addService(this.hap.Service.ContactSensor)), contactSensorService; - - this.contactSensorService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Contact Sensor`); - if (!this.contactSensorService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.contactSensorService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Contact Sensor`); + if (this.ContactSensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Contact Sensor Service`); + this.ContactSensor.Service = this.accessory.getService(this.hap.Service.ContactSensor) as Service; + accessory.removeService(this.ContactSensor.Service); } } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Contact Sensor Service Not Added`); + accessory.context.ContactSensor = accessory.context.ContactSensor ?? {}; + this.ContactSensor = { + Name: accessory.context.ContactSensor.Name ?? `${accessory.displayName} Contact Sensor`, + Service: accessory.getService(this.hap.Service.ContactSensor) ?? this.accessory.addService(this.hap.Service.ContactSensor) as Service, + ContactSensorState: accessory.context.ContactSensorState ?? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED, + }; + accessory.context.ContactSensor = this.ContactSensor as object; + + // Initialize Contact Sensor Characteristics + this.ContactSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.ContactSensor.Name) + .setCharacteristic(this.hap.Characteristic.StatusActive, true) + .getCharacteristic(this.hap.Characteristic.ContactSensorState) + .onGet(() => { + return this.ContactSensor!.ContactSensorState; + }); } - // Battery Service - const batteryService = `${accessory.displayName} Battery`; - (this.batteryService = this.accessory.getService(this.hap.Service.Battery) - || accessory.addService(this.hap.Service.Battery)), batteryService; - - this.batteryService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Battery`); - if (!this.batteryService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.batteryService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Battery`); + // Initialize Latch Button Service + if (device.lock?.activate_latchbutton === false) { + if (this.Switch) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Latch Button Service`); + this.Switch.Service = accessory.getService(this.hap.Service.Switch) as Service; + accessory.removeService(this.Switch.Service); + } + } else { + accessory.context.Switch = accessory.context.Switch ?? {}; + this.Switch = { + Name: accessory.context.Switch.Name ?? `${accessory.displayName} Latch`, + Service: accessory.getService(this.hap.Service.Switch) ?? accessory.addService(this.hap.Service.Switch) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Switch = this.Switch as object; + + // Initialize Latch Button Characteristics + this.Switch.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Switch.Name) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.Switch!.On; + }) + .onSet(this.OnSet.bind(this)); } - this.batteryService.setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE); + + // Retrieve initial values and updateHomekit + this.refreshStatus(); // Update Homekit this.updateHomeKitCharacteristics(); @@ -181,25 +160,7 @@ export class Lock { }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - const { lockState } = context; - const { LockCurrentState } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(lockState) = ' + - `Webhook:(${lockState}), ` + - `current:(${LockCurrentState})`); - this.LockCurrentState = lockState === 'LOCKED' ? 1 : 0; - this.updateHomeKitCharacteristics(); - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); // Watch for Lock change events // We put in a debounce of 100ms so we don't make duplicate calls @@ -208,150 +169,105 @@ export class Lock { tap(() => { this.lockUpdateInProgress = true; }), - debounceTime(this.platform.config.options!.pushRate! * 1000), + debounceTime(this.devicePushRate * 1000), ) .subscribe(async () => { try { await this.pushChanges(); } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + - ` Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + + ` Error Message: ${JSON.stringify(e.message)}`); } this.lockUpdateInProgress = false; }); } - /** - * Method for handling the LatchCharacteristic - */ - async handleLatchCharacteristic(value: boolean, callback) { - this.debugLog(`handleLatchCharacteristic called with value: ${value}`); - - if (value) { - this.debugLog('Attempting to open the latch'); - - this.openAPIpushChanges(value).then(() => { - this.debugLog('Latch opened successfully'); - this.debugLog(`LatchButtonService is: ${this.latchButtonService ? 'available' : 'not available'}`); - - // simulate button press to turn the switch back off - if (this.latchButtonService) { - const latchButtonService = this.latchButtonService; - // Simulate a button press by waiting a short period before turning the switch off - setTimeout(() => { - latchButtonService.getCharacteristic(this.hap.Characteristic.On).updateValue(false); - this.debugLog('Latch button switched off automatically.'); - }, 500); // 500 ms delay - } - callback(null); - }).catch((error) => { - // Log the error if the operation failed - this.debugLog(`Error opening latch: ${error}`); - - // Ensure we turn the switch back off even in case of an error - if (this.latchButtonService) { - this.latchButtonService.getCharacteristic(this.hap.Characteristic.On).updateValue(false); - this.debugLog('Latch button switched off after an error.'); - } - - callback(error); - }); - - } else { - this.debugLog('Switch is off, nothing to do'); - callback(null); - } - } - - - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - /* } else if (this.BLE) { - await this.BLEparseStatus();*/ - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { + // BLE Status this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); - switch (this.BLE_LockCurrentState) { + switch (serviceData.status) { case 'locked': - this.LockCurrentState = this.hap.Characteristic.LockCurrentState.SECURED; - this.LockTargetState = this.hap.Characteristic.LockTargetState.SECURED; + this.LockMechanism.LockCurrentState = this.hap.Characteristic.LockCurrentState.SECURED; + this.LockMechanism.LockTargetState = this.hap.Characteristic.LockTargetState.SECURED; break; default: - this.LockCurrentState = this.hap.Characteristic.LockCurrentState.UNSECURED; - this.LockTargetState = this.hap.Characteristic.LockTargetState.UNSECURED; + this.LockMechanism.LockCurrentState = this.hap.Characteristic.LockCurrentState.UNSECURED; + this.LockMechanism.LockTargetState = this.hap.Characteristic.LockTargetState.UNSECURED; } - switch (this.BLE_ContactSensorState) { - case 'opened': - this.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; - break; - default: - this.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` LockTargetState: ${this.LockMechanism.LockTargetState}, LockCurrentState: ${this.LockMechanism.LockCurrentState}`); + + // Contact Sensor + if (!this.device.lock?.hide_contactsensor) { + switch (serviceData.door_open) { + case 'opened': + this.ContactSensor!.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; + break; + default: + this.ContactSensor!.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED; + } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ContactSensorState: ${this.ContactSensor!.ContactSensorState}`); + } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LockTargetState}`); // Battery - this.BatteryLevel = Number(this.BLE_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(serviceData.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` + ` StatusLowBattery: ${this.StatusLowBattery}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` BatteryLevel: ${this.Battery.BatteryLevel}, StatusLowBattery: ${this.Battery.StatusLowBattery}`); } - async openAPIparseStatus(): Promise { + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); - switch (this.OpenAPI_LockCurrentState) { + switch (deviceStatus.body.lockState) { case 'locked': - this.LockCurrentState = this.hap.Characteristic.LockCurrentState.SECURED; - this.LockTargetState = this.hap.Characteristic.LockTargetState.SECURED; + this.LockMechanism.LockCurrentState = this.hap.Characteristic.LockCurrentState.SECURED; + this.LockMechanism.LockTargetState = this.hap.Characteristic.LockTargetState.SECURED; break; default: - this.LockCurrentState = this.hap.Characteristic.LockCurrentState.UNSECURED; - this.LockTargetState = this.hap.Characteristic.LockTargetState.UNSECURED; + this.LockMechanism.LockCurrentState = this.hap.Characteristic.LockCurrentState.UNSECURED; + this.LockMechanism.LockTargetState = this.hap.Characteristic.LockTargetState.UNSECURED; } - switch (this.OpenAPI_ContactSensorState) { + switch (deviceStatus.body.doorState) { case 'opened': - this.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; + this.ContactSensor!.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; break; default: - this.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED; + this.ContactSensor!.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LockTargetState}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LockMechanism.LockTargetState}`); // Battery - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - if (Number.isNaN(this.BatteryLevel)) { - this.BatteryLevel = 100; + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` - + ` StatusLowBattery: ${this.StatusLowBattery}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); + } } /** @@ -366,10 +282,8 @@ export class Lock { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -386,103 +300,43 @@ export class Lock { // Start to monitor advertisement packets (async () => { // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'o', - id: this.device.bleMac, - }); + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === 'o') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_BatteryLevel = ad.serviceData.battery; - this.BLE_Calibration = ad.serviceData.calibration; - this.BLE_LockCurrentState = ad.serviceData.status; - this.BLE_ContactSensorState = ad.serviceData.door_open; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: 'o', - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.BLE_BatteryLevel = ad.serviceData.battery; - this.BLE_Calibration = ad.serviceData.calibration; - this.BLE_LockCurrentState = ad.serviceData.status; - this.BLE_ContactSensorState = ad.serviceData.door_open; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} battery: ${ad.serviceData.battery}, ` + - `calibration: ${ad.serviceData.calibration}, status: ${ad.serviceData.status}, battery: ${ad.serviceData.battery}, ` + - `door_open: ${ad.serviceData.door_open}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_LockCurrentState = deviceStatus.body.lockState; - this.OpenAPI_ContactSensorState = deviceStatus.body.doorState; - this.OpenAPI_BatteryLevel = deviceStatus.body.battery; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -490,10 +344,28 @@ export class Lock { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { lockState } = context; + const { LockCurrentState } = this.LockMechanism; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (lockState) = Webhook:(${lockState}), current:(${LockCurrentState})`); + this.LockMechanism.LockCurrentState = lockState === 'LOCKED' ? 1 : 0; + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -506,15 +378,14 @@ export class Lock { async pushChanges(): Promise { if (!this.device.enableCloudService && this.OpenAPI) { this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} pushChanges enableCloudService: ${this.device.enableCloudService}`); - /*} else if (this.BLE) { - await this.BLEpushChanges();*/ + } else if (this.BLE) { + await this.BLEpushChanges(); } else if (this.OpenAPI && this.platform.config.credentials?.token) { await this.openAPIpushChanges(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, pushChanges will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, pushChanges will not happen.`); } // Refresh the status from the API interval(15000) @@ -527,11 +398,9 @@ export class Lock { async BLEpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`); - if (this.LockTargetState !== this.accessory.context.LockTargetState) { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges LockTargetState: ${this.LockTargetState}` + - ` LockTargetStateCached: ${this.accessory.context.LockTargetState}`, - ); + if (this.LockMechanism.LockTargetState !== this.accessory.context.LockTargetState) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges LockTargetState: ${this.LockMechanism.LockTargetState}` + + ` LockTargetStateCached: ${this.accessory.context.LockTargetState}`); const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -546,35 +415,33 @@ export class Lock { }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.LockTargetState = this.hap.Characteristic.LockTargetState.SECURED; + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `LockTargetState: ${this.LockMechanism.LockTargetState} sent over BLE, sent successfully`); + this.LockMechanism.LockTargetState = this.hap.Characteristic.LockTargetState.SECURED; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges.` + - `LockTargetState: ${this.LockTargetState}, ` + - `LockTargetStateCached: ${this.accessory.context.LockTargetState}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges.` + + `LockTargetState: ${this.LockMechanism.LockTargetState}, ` + + `LockTargetStateCached: ${this.accessory.context.LockTargetState}`); } } async openAPIpushChanges(LatchUnlock?): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`); - if ((this.LockTargetState !== this.accessory.context.LockTargetState) || LatchUnlock) { + if ((this.LockMechanism.LockTargetState !== this.accessory.context.LockTargetState) || LatchUnlock) { // Determine the command based on the LockTargetState or the forceUnlock parameter let command = ''; if (LatchUnlock) { command = 'unlock'; } else { - command = this.LockTargetState ? 'lock' : 'unlock'; + command = this.LockMechanism.LockTargetState ? 'lock' : 'unlock'; } const bodyChange = JSON.stringify({ command: `${command}`, @@ -596,23 +463,20 @@ export class Lock { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.` + - `LockTargetState: ${this.LockTargetState}, ` + - `LockTargetStateCached: ${this.accessory.context.LockTargetState}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges, LockTargetState: ` + + `${this.LockMechanism.LockTargetState}, LockTargetStateCached: ${this.accessory.context.LockTargetState}`); } } @@ -620,81 +484,97 @@ export class Lock { * Handle requests to set the value of the "On" characteristic */ async LockTargetStateSet(value: CharacteristicValue): Promise { - if (this.LockTargetState === this.accessory.context.LockTargetState) { + if (this.LockMechanism.LockTargetState === this.accessory.context.LockTargetState) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set LockTargetState: ${value}`); } else { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set LockTargetState: ${value}`); } - this.LockTargetState = value; + this.LockMechanism.LockTargetState = value; + this.doLockUpdate.next(); + } + + /** + * Handle requests to set the value of the "On" characteristic + */ + async OnSet(value: CharacteristicValue): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Latch Button Set On: ${value}`); + if (value) { + this.debugLog('Attempting to open the latch'); + + this.openAPIpushChanges(value).then(() => { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Latch opened successfully`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` SwitchService is: ${this.Switch!.Service ? 'available' : 'not available'}`); + + // simulate button press to turn the switch back off + if (this.Switch!.Service) { + const SwitchService = this.Switch!.Service; + // Simulate a button press by waiting a short period before turning the switch off + setTimeout(() => { + SwitchService.getCharacteristic(this.hap.Characteristic.On).updateValue(false); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Latch button switched off automatically.`); + }, 500); // 500 ms delay + } + }).catch((e: any) => { + // Log the error if the operation failed + this.debugLog(`Error opening latch: ${e}`); + // Ensure we turn the switch back off even in case of an error + if (this.Switch!.Service) { + this.Switch!.Service.getCharacteristic(this.hap.Characteristic.On).updateValue(false); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Latch button switched off after an error.`); + } + }); + } else { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Switch is off, nothing to do`); + } + + this.Switch!.On = value; this.doLockUpdate.next(); } async updateHomeKitCharacteristics(): Promise { if (!this.device.lock?.hide_contactsensor) { - if (this.ContactSensorState === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ContactSensorState: ${this.ContactSensorState}`); + if (this.ContactSensor!.ContactSensorState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ContactSensorState: ${this.ContactSensor!.ContactSensorState}`); } else { - this.accessory.context.ContactSensorState = this.ContactSensorState; - this.contactSensorService?.updateCharacteristic(this.hap.Characteristic.ContactSensorState, this.ContactSensorState); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic ContactSensorState: ${this.ContactSensorState}`); + this.accessory.context.ContactSensorState = this.ContactSensor!.ContactSensorState; + this.ContactSensor!.Service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, this.ContactSensor!.ContactSensorState); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` ContactSensorState: ${this.ContactSensor!.ContactSensorState}`); } } - if (this.LockTargetState === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LockTargetState: ${this.LockTargetState}`); + if (this.LockMechanism.LockTargetState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LockTargetState: ${this.LockMechanism.LockTargetState}`); } else { - this.accessory.context.LockTargetState = this.LockTargetState; - this.lockService.updateCharacteristic(this.hap.Characteristic.LockTargetState, this.LockTargetState); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic LockTargetState: ${this.LockTargetState}`); + this.accessory.context.LockTargetState = this.LockMechanism.LockTargetState; + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockTargetState, this.LockMechanism.LockTargetState); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` LockTargetState: ${this.LockMechanism.LockTargetState}`); } - if (this.LockCurrentState === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LockCurrentState: ${this.LockCurrentState}`); + if (this.LockMechanism.LockCurrentState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LockCurrentState: ${this.LockMechanism.LockCurrentState}`); } else { - this.accessory.context.LockCurrentState = this.LockCurrentState; - this.lockService.updateCharacteristic(this.hap.Characteristic.LockCurrentState, this.LockCurrentState); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic LockCurrentState: ${this.LockCurrentState}`); + this.accessory.context.LockCurrentState = this.LockMechanism.LockCurrentState; + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, this.LockMechanism.LockCurrentState); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` LockCurrentState: ${this.LockMechanism.LockCurrentState}`); } - if (this.BatteryLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}`); + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); } else { - this.accessory.context.BatteryLevel = this.BatteryLevel; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.BatteryLevel); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` BatteryLevel: ${this.Battery.BatteryLevel}`); } - if (this.StatusLowBattery === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); } else { - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); - } - } - - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); - } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'c', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); } } @@ -714,227 +594,22 @@ export class Lock { } } - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); + if (!this.device.lock?.hide_contactsensor) { + this.ContactSensor!.Service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, + this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED); + } + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockTargetState, this.hap.Characteristic.LockTargetState.SECURED); + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, this.hap.Characteristic.LockCurrentState.SECURED); } } async apiError(e: any): Promise { if (!this.device.lock?.hide_contactsensor) { - this.contactSensorService?.updateCharacteristic(this.hap.Characteristic.ContactSensorState, e); - } - this.lockService.updateCharacteristic(this.hap.Characteristic.LockTargetState, e); - this.lockService.updateCharacteristic(this.hap.Characteristic.LockCurrentState, e); - } - - async deviceContext() { - if (this.LockTargetState === undefined) { - this.LockTargetState = false; - } else { - this.LockTargetState = this.accessory.context.On; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.lock) { - config = device.lock; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); + this.ContactSensor!.Service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, e); } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockTargetState, e); + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, e); } } diff --git a/src/device/meter.ts b/src/device/meter.ts index 4677e55f..15c4e485 100644 --- a/src/device/meter.ts +++ b/src/device/meter.ts @@ -1,112 +1,96 @@ -import asyncmqtt from 'async-mqtt'; -import { CharacteristicValue, PlatformAccessory, Service, Units, API, Logging, HAP } from 'homebridge'; -import { MqttClient } from 'mqtt'; -import { hostname } from 'os'; -import { interval } from 'rxjs'; -import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, device, deviceStatus, devicesConfig, serviceData, temperature, SwitchBotPlatformConfig } from '../settings.js'; -import { sleep } from '../utils.js'; - -export class Meter { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * meter.ts: @switchbot/homebridge-switchbot. + */ +import { Units } from 'homebridge'; +import { Devices } from '../settings.js'; +import { deviceBase } from './device.js'; +import { convertUnits } from '../utils.js'; +import { Subject, interval, skipWhile } from 'rxjs'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; + +export class Meter extends deviceBase { // Services - batteryService: Service; - humidityService?: Service; - temperatureService?: Service; - - // Characteristic Values - BatteryLevel!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - StatusLowBattery!: CharacteristicValue; - CurrentTemperature?: CharacteristicValue; - CurrentRelativeHumidity?: CharacteristicValue; - - // OpenAPI Status - OpenAPI_BatteryLevel: deviceStatus['battery']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_CurrentTemperature: deviceStatus['temperature']; - OpenAPI_CurrentRelativeHumidity: deviceStatus['humidity']; - - // BLE Status - BLE_Celsius!: temperature['c']; - BLE_Fahrenheit!: temperature['f']; - BLE_BatteryLevel!: serviceData['battery']; - BLE_CurrentTemperature!: serviceData['temperature']; - BLE_CurrentRelativeHumidity!: serviceData['humidity']; - - // BLE Others - BLE_IsConnected?: boolean; - - //MQTT stuff - mqttClient: MqttClient | null = null; - - // EVE history service handler - historyService?: any; - - // Config - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; - - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; + private Battery: { + Name: CharacteristicValue; + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + }; + + private HumiditySensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentRelativeHumidity: CharacteristicValue; + }; + + private TemperatureSensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentTemperature: CharacteristicValue; + }; + + // Updates + meterUpdateInProgress!: boolean; + doMeterUpdate: Subject; constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); - this.deviceContext(); - this.setupHistoryService(device); - this.setupMqtt(device); - this.deviceConfig(device); - - this.CurrentRelativeHumidity = accessory.context.CurrentRelativeHumidity; - this.CurrentTemperature = accessory.context.CurrentTemperature; - - // Retrieve initial values and updateHomekit - this.refreshStatus(); + super(platform, accessory, device); + // this is subject we use to track when we need to POST changes to the SwitchBot API + this.doMeterUpdate = new Subject(); + this.meterUpdateInProgress = false; + + // Initialize Battery Service + accessory.context.Battery = accessory.context.Battery ?? {}; + this.Battery = { + Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Characteristics + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE) + .getCharacteristic(this.hap.Characteristic.BatteryLevel) + .onGet(() => { + return this.Battery.BatteryLevel; + }); - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, 'METERTH-S1') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); + this.Battery.Service + .getCharacteristic(this.hap.Characteristic.StatusLowBattery) + .onGet(() => { + return this.Battery.StatusLowBattery; + }); - // Temperature Sensor Service + // Initialize Temperature Sensor Service if (device.meter?.hide_temperature) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Temperature Sensor Service`); - this.temperatureService = this.accessory.getService(this.hap.Service.TemperatureSensor); - accessory.removeService(this.temperatureService!); - } else if (!this.temperatureService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Temperature Sensor Service`); - const temperatureService = `${accessory.displayName} Temperature Sensor`; - (this.temperatureService = this.accessory.getService(this.hap.Service.TemperatureSensor) - || this.accessory.addService(this.hap.Service.TemperatureSensor)), temperatureService; - - this.temperatureService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Temperature Sensor`); - if (!this.temperatureService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.temperatureService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Temperature Sensor`); + if (this.TemperatureSensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Temperature Sensor Service`); + this.TemperatureSensor.Service = this.accessory.getService(this.hap.Service.TemperatureSensor) as Service; + accessory.removeService(this.TemperatureSensor.Service); } - this.temperatureService + } else { + accessory.context.TemperatureSensor = accessory.context.TemperatureSensor ?? {}; + this.TemperatureSensor = { + Name: accessory.context.TemperatureSensor.Name ?? `${accessory.displayName} Temperature Sensor`, + Service: accessory.getService(this.hap.Service.TemperatureSensor) ?? this.accessory.addService(this.hap.Service.TemperatureSensor) as Service, + CurrentTemperature: accessory.context.CurrentTemperature ?? 30, + }; + accessory.context.TemperatureSensor = this.TemperatureSensor as object; + + // Initialize Temperature Sensor Characteristics + this.TemperatureSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.TemperatureSensor.Name) .getCharacteristic(this.hap.Characteristic.CurrentTemperature) .setProps({ unit: Units['CELSIUS'], @@ -116,168 +100,122 @@ export class Meter { minStep: 0.1, }) .onGet(() => { - return this.CurrentTemperature!; + return this.TemperatureSensor!.CurrentTemperature!; }); - } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Temperature Sensor Service Not Added`); } - - // Humidity Sensor Service + // Initialize Humidity Sensor Service if (device.meter?.hide_humidity) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Humidity Sensor Service`); - this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor); - accessory.removeService(this.humidityService!); - } else if (!this.humidityService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Humidity Sensor Service`); - const humidityService = `${accessory.displayName} Humidity Sensor`; - (this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor) - || this.accessory.addService(this.hap.Service.HumiditySensor)), humidityService; - - this.humidityService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Humidity Sensor`); - if (!this.humidityService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.humidityService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Humidity Sensor`); + if (this.HumiditySensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Humidity Sensor Service`); + this.HumiditySensor.Service = this.accessory.getService(this.hap.Service.HumiditySensor) as Service; + accessory.removeService(this.HumiditySensor.Service); } - this.humidityService + } else { + accessory.context.HumiditySensor = accessory.context.HumiditySensor ?? {}; + this.HumiditySensor = { + Name: accessory.context.HumiditySensorName ?? `${accessory.displayName} Humidity Sensor`, + Service: accessory.getService(this.hap.Service.HumiditySensor) ?? this.accessory.addService(this.hap.Service.HumiditySensor) as Service, + CurrentRelativeHumidity: accessory.context.CurrentRelativeHumidity ?? 50, + }; + accessory.context.HumiditySensor = this.HumiditySensor as object; + + // Initialize Humidity Sensor Characteristics + this.HumiditySensor!.Service + .setCharacteristic(this.hap.Characteristic.Name, this.HumiditySensor.Name) .getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity) .setProps({ minStep: 0.1, }) .onGet(() => { - return this.CurrentRelativeHumidity!; + return this.HumiditySensor!.CurrentRelativeHumidity!; }); - } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Humidity Sensor Service Not Added`); } - // Battery Service - const batteryService = `${accessory.displayName} Battery`; - (this.batteryService = this.accessory.getService(this.hap.Service.Battery) - || accessory.addService(this.hap.Service.Battery)), batteryService; - - this.batteryService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Battery`); - if (!this.batteryService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.batteryService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Battery`); - } - this.batteryService.setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE); + // Retrieve initial values and updateHomekit + this.refreshStatus(); // Retrieve initial values and update Homekit this.updateHomeKitCharacteristics(); // Start an update interval interval(this.deviceRefreshRate * 1000) + .pipe(skipWhile(() => this.meterUpdateInProgress)) .subscribe(async () => { await this.refreshStatus(); }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - if (context.scale === 'CELSIUS') { - const { temperature, humidity } = context; - const { CurrentTemperature, CurrentRelativeHumidity } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(temperature, humidity) = ' + - `Webhook:(${temperature}, ${humidity}), ` + - `current:(${CurrentTemperature}, ${CurrentRelativeHumidity})`); - this.CurrentRelativeHumidity = humidity; - this.CurrentTemperature = temperature; - this.updateHomeKitCharacteristics(); - } - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } - } - - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } + this.registerWebhook(accessory, device); } - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // BatteryLevel - this.BatteryLevel = Number(this.BLE_BatteryLevel); - if (this.BatteryLevel < 15) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(serviceData.battery); + if (this.Battery.BatteryLevel < 15) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}, StatusLowBattery: ${this.StatusLowBattery}`); + this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}, StatusLowBattery: ${this.Battery.StatusLowBattery}`); // CurrentRelativeHumidity if (!this.device.meter?.hide_humidity) { - this.CurrentRelativeHumidity = this.BLE_CurrentRelativeHumidity!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.CurrentRelativeHumidity}%`); + this.HumiditySensor!.CurrentRelativeHumidity = serviceData.humidity!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.HumiditySensor!.CurrentRelativeHumidity}%`); } // CurrentTemperature if (!this.device.meter?.hide_temperature) { - this.BLE_Celsius < 0 ? 0 : this.BLE_Celsius > 100 ? 100 : this.BLE_Celsius; - this.CurrentTemperature = this.BLE_Celsius; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.CurrentTemperature}°c`); + serviceData.temperature!.c < 0 ? 0 : serviceData.temperature!.c > 100 ? 100 : serviceData.temperature!.c; + this.TemperatureSensor!.CurrentTemperature = serviceData.temperature!.c; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.TemperatureSensor!.CurrentTemperature}°c`); } } - async openAPIparseStatus(): Promise { + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); - // BatteryLevel - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 15) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + // Battery + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + } + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; } - this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}, StatusLowBattery: ${this.StatusLowBattery}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); // CurrentRelativeHumidity if (!this.device.meter?.hide_humidity) { - this.CurrentRelativeHumidity = this.OpenAPI_CurrentRelativeHumidity!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.CurrentRelativeHumidity}%`); + this.HumiditySensor!.CurrentRelativeHumidity = deviceStatus.body.humidity!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.HumiditySensor!.CurrentRelativeHumidity}%`); } // CurrentTemperature if (!this.device.meter?.hide_temperature) { - this.CurrentTemperature = this.OpenAPI_CurrentTemperature!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.CurrentTemperature}°c`); + this.TemperatureSensor!.CurrentTemperature = deviceStatus.body.temperature!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.TemperatureSensor!.CurrentTemperature}°c`); + } + + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); } - - // Battery - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; - } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - } - if (Number.isNaN(this.BatteryLevel)) { - this.BatteryLevel = 100; - } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` - + ` StatusLowBattery: ${this.StatusLowBattery}`); - - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; } /** @@ -292,10 +230,8 @@ export class Meter { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -311,108 +247,44 @@ export class Meter { this.getCustomBLEAddress(switchbot); // Start to monitor advertisement packets (async () => { - await switchbot.startScan({ - model: 'T', - id: this.device.bleMac, - }); + // Start to monitor advertisement packets + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === 'T') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_CurrentRelativeHumidity = ad.serviceData.humidity < 0 ? 0 : ad.serviceData.humidity > 100 ? 100 : ad.serviceData.humidity; - this.BLE_CurrentTemperature = ad.serviceData.temperature; - this.BLE_Celsius = ad.serviceData.temperature!.c; - this.BLE_Fahrenheit = ad.serviceData.temperature!.f; - this.BLE_BatteryLevel = ad.serviceData.battery; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: 'T', - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - if (ad.serviceData.humidity! > 0) { - // reject unreliable data - this.BLE_CurrentRelativeHumidity = ad.serviceData.humidity; - } - this.BLE_CurrentTemperature = ad.serviceData.temperature; - this.BLE_Celsius = ad.serviceData.temperature!.c; - this.BLE_Fahrenheit = ad.serviceData.temperature!.f; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} model: ${ad.serviceData.model}, modelName: ${ad.serviceData.modelName}, ` + - `temperature: ${JSON.stringify(ad.serviceData.temperature?.c)}, humidity: ${ad.serviceData.humidity}, ` + - `battery: ${ad.serviceData.battery}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_CurrentRelativeHumidity = deviceStatus.body.humidity!; - this.OpenAPI_CurrentTemperature = deviceStatus.body.temperature!; - this.OpenAPI_BatteryLevel = deviceStatus.body.battery; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -420,10 +292,41 @@ export class Meter { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { temperature, humidity } = context; + const { CurrentTemperature } = this.TemperatureSensor ?? { CurrentTemperature: undefined }; + const { CurrentRelativeHumidity } = this.HumiditySensor ?? { CurrentRelativeHumidity: undefined }; + if (context.scale !== 'CELCIUS' && device.meter?.convertUnitTo === undefined) { + this.warnLog(`${device.deviceType}: ${accessory.displayName} received Webhook scale: ` + + `${context.scale}, instead of CELCIUS. Use the *convertUnitsTo* config under Meter settings, if displaying incorrectly in HomeKit.`); + } + this.debugLog(`${device.deviceType}: ${accessory.displayName} ` + + '(scale, temperature, humidity) = ' + + `Webhook:(${context.scale}, ${convertUnits(temperature, context.scale, device.meter?.convertUnitTo)}, ${humidity}), ` + + `current:(${CurrentTemperature}, ${CurrentRelativeHumidity})`); + if (!device.meter?.hide_humidity) { + this.HumiditySensor!.CurrentRelativeHumidity = humidity; + } + if (!device.meter?.hide_temperature) { + this.TemperatureSensor!.CurrentTemperature = convertUnits(temperature, context.scale, device.meter?.convertUnitTo); + } + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -436,64 +339,72 @@ export class Meter { // CurrentRelativeHumidity if (!this.device.meter?.hide_humidity) { - if (this.CurrentRelativeHumidity === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + if (this.HumiditySensor!.CurrentRelativeHumidity === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}`); } else { + this.accessory.context.CurrentRelativeHumidity = this.HumiditySensor!.CurrentRelativeHumidity; + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, + this.HumiditySensor!.CurrentRelativeHumidity); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` updateCharacteristic CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}`); if (this.device.mqttURL) { - mqttmessage.push(`"humidity": ${this.CurrentRelativeHumidity}`); + mqttmessage.push(`"humidity": ${this.HumiditySensor!.CurrentRelativeHumidity}`); } if (this.device.history) { - entry['humidity'] = this.CurrentRelativeHumidity; + entry['humidity'] = this.HumiditySensor!.CurrentRelativeHumidity; } - this.accessory.context.CurrentRelativeHumidity = this.CurrentRelativeHumidity; - this.humidityService?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.CurrentRelativeHumidity); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `updateCharacteristic CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); } } + // CurrentTemperature if (!this.device.meter?.hide_temperature) { - if (this.CurrentTemperature === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}`); + if (this.TemperatureSensor!.CurrentTemperature === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); } else { + this.accessory.context.CurrentTemperature = this.TemperatureSensor!.CurrentTemperature; + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.TemperatureSensor!.CurrentTemperature); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); if (this.device.mqttURL) { - mqttmessage.push(`"temperature": ${this.CurrentTemperature}`); + mqttmessage.push(`"temperature": ${this.TemperatureSensor!.CurrentTemperature}`); } if (this.device.history) { - entry['temp'] = this.CurrentTemperature; + entry['temp'] = this.TemperatureSensor!.CurrentTemperature; } - this.accessory.context.CurrentTemperature = this.CurrentTemperature; - this.temperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.CurrentTemperature); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.CurrentTemperature}`); } } + // BatteryLevel - if (this.BatteryLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}`); + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); } else { + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery!.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); if (this.device.mqttURL) { - mqttmessage.push(`"battery": ${this.BatteryLevel}`); + mqttmessage.push(`"battery": ${this.Battery.BatteryLevel}`); } - this.accessory.context.BatteryLevel = this.BatteryLevel; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.BatteryLevel); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); } - if (this.StatusLowBattery === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); } else { + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery!.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); if (this.device.mqttURL) { - mqttmessage.push(`"lowBattery": ${this.StatusLowBattery}`); + mqttmessage.push(`"lowBattery": ${this.Battery.StatusLowBattery}`); } - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); } - // MQTT + // MQTT Publish if (this.device.mqttURL) { this.mqttPublish(`{${mqttmessage.join(',')}}`); } - if (Number(this.CurrentRelativeHumidity) > 0) { + + // History Service + if (!this.device.meter?.hide_humidity && (Number(this.HumiditySensor!.CurrentRelativeHumidity) > 0)) { // reject unreliable data if (this.device.history) { this.historyService?.addEntry(entry); @@ -501,84 +412,6 @@ export class Meter { } } - /* - * Publish MQTT message for topics of - * 'homebridge-switchbot/meter/xx:xx:xx:xx:xx:xx' - */ - mqttPublish(message: any) { - const mac = this.device.deviceId - ?.toLowerCase() - .match(/[\s\S]{1,2}/g) - ?.join(':'); - const options = this.device.mqttPubOptions || {}; - this.mqttClient?.publish(`homebridge-switchbot/meter/${mac}`, `${message}`, options); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT message: ${message} options:${JSON.stringify(options)}`); - } - - /* - * Setup MQTT hadler if URL is specified. - */ - async setupMqtt(device: device & devicesConfig): Promise { - if (device.mqttURL) { - try { - const { connectAsync } = asyncmqtt; - this.mqttClient = await connectAsync(device.mqttURL, device.mqttOptions || {}); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT connection has been established successfully.`); - this.mqttClient.on('error', (e: Error) => { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to publish MQTT messages. ${e}`); - }); - } catch (e) { - this.mqttClient = null; - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to establish MQTT connection. ${e}`); - } - } - } - - /* - * Setup EVE history graph feature if enabled. - */ - async setupHistoryService(device: device & devicesConfig): Promise { - const mac = this.device - .deviceId!.match(/.{1,2}/g)! - .join(':') - .toLowerCase(); - this.historyService = device.history - ? new this.platform.fakegatoAPI('room', this.accessory, { - log: this.platform.log, - storage: 'fs', - filename: `${hostname().split('.')[0]}_${mac}_persist.json`, - }) - : null; - } - - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); - } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'T', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); - } - } - async BLERefreshConnection(switchbot: any): Promise { this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} wasn't able to establish BLE Connection, node-switchbot:` + ` ${JSON.stringify(switchbot)}`); @@ -588,249 +421,26 @@ export class Meter { } } - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); + if (!this.device.meter?.hide_humidity) { + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, 50); + } + if (!this.device.meter?.hide_temperature) { + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, 30); + } + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, 100); } } async apiError(e: any): Promise { if (!this.device.meter?.hide_humidity) { - this.humidityService?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); } if (!this.device.meter?.hide_temperature) { - this.temperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); - } - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); - } - - async deviceContext() { - if (this.CurrentRelativeHumidity === undefined) { - this.CurrentRelativeHumidity = 0; - } else { - this.CurrentRelativeHumidity = this.accessory.context.CurrentRelativeHumidity; - } - if (this.CurrentTemperature === undefined) { - this.CurrentTemperature = 0; - } else { - this.CurrentTemperature = this.accessory.context.CurrentTemperature; - } - if (this.BatteryLevel === undefined) { - this.BatteryLevel = 100; - } else { - this.BatteryLevel = this.accessory.context.BatteryLevel; - } - if (this.StatusLowBattery === undefined) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - } else { - this.StatusLowBattery = this.accessory.context.StatusLowBattery; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.meter) { - config = device.meter; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.mqttURL !== undefined) { - config['mqttURL'] = device.mqttURL; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.Battery!.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); + this.Battery!.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); } } diff --git a/src/device/meterplus.ts b/src/device/meterplus.ts index 49ae0309..b75063ba 100644 --- a/src/device/meterplus.ts +++ b/src/device/meterplus.ts @@ -1,114 +1,100 @@ -import asyncmqtt from 'async-mqtt'; -import { CharacteristicValue, PlatformAccessory, Service, Units, API, Logging, HAP } from 'homebridge'; -import { MqttClient } from 'mqtt'; -import { hostname } from 'os'; -import { interval } from 'rxjs'; -import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, device, deviceStatus, devicesConfig, serviceData, temperature, SwitchBotPlatformConfig } from '../settings.js'; -import { sleep } from '../utils.js'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * meterplus.ts: @switchbot/homebridge-switchbot. + */ +import { Units } from 'homebridge'; +import { Devices } from '../settings.js'; +import { deviceBase } from './device.js'; +import { Subject, interval, skipWhile } from 'rxjs'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class MeterPlus { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class MeterPlus extends deviceBase { // Services - batteryService: Service; - humidityService?: Service; - temperatureService?: Service; - - // Characteristic Values - BatteryLevel!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - StatusLowBattery!: CharacteristicValue; - CurrentTemperature?: CharacteristicValue; - CurrentRelativeHumidity?: CharacteristicValue; - - // OpenAPI Status - OpenAPI_BatteryLevel: deviceStatus['battery']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_CurrentTemperature: deviceStatus['temperature']; - OpenAPI_CurrentRelativeHumidity: deviceStatus['humidity']; - - // BLE Status - BLE_Celsius!: temperature['c']; - BLE_Fahrenheit!: temperature['f']; - BLE_BatteryLevel!: serviceData['battery']; - BLE_CurrentTemperature!: serviceData['temperature']; - BLE_CurrentRelativeHumidity!: serviceData['humidity']; - - // BLE Others - BLE_IsConnected?: boolean; - - //MQTT stuff - mqttClient: MqttClient | null = null; - - // EVE history service handler - historyService?: any; - - // Config - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; - - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; + private Battery: { + Name: CharacteristicValue; + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + }; + + private HumiditySensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentRelativeHumidity: CharacteristicValue; + }; + + private TemperatureSensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentTemperature: CharacteristicValue; + }; + + // Updates + meterUpdateInProgress!: boolean; + doMeterUpdate: Subject; constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); - this.deviceContext(); - this.setupHistoryService(device); - this.setupMqtt(device); - this.deviceConfig(device); - - // Retrieve initial values and updateHomekit - this.refreshStatus(); + super(platform, accessory, device); + // this is subject we use to track when we need to POST changes to the SwitchBot API + this.doMeterUpdate = new Subject(); + this.meterUpdateInProgress = false; + + // Initialize Battery Service + accessory.context.Battery = accessory.context.Battery ?? {}; + this.Battery = { + Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Characteristics + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE) + .getCharacteristic(this.hap.Characteristic.BatteryLevel) + .onGet(() => { + return this.Battery.BatteryLevel; + }); - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, this.model(device)) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); + this.Battery.Service + .getCharacteristic(this.hap.Characteristic.StatusLowBattery) + .onGet(() => { + return this.Battery.StatusLowBattery; + }); - // Temperature Sensor Service + // Initialize Temperature Sensor Service if (device.meter?.hide_temperature) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Temperature Sensor Service`); - this.temperatureService = this.accessory.getService(this.hap.Service.TemperatureSensor); - accessory.removeService(this.temperatureService!); - } else if (!this.temperatureService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Temperature Sensor Service`); - const temperatureService = `${accessory.displayName} Temperature Sensor`; - (this.temperatureService = this.accessory.getService(this.hap.Service.TemperatureSensor) - || this.accessory.addService(this.hap.Service.TemperatureSensor)), temperatureService; - - this.temperatureService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Temperature Sensor`); - if (!this.temperatureService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.temperatureService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Temperature Sensor`); + if (this.TemperatureSensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Temperature Sensor Service`); + this.TemperatureSensor.Service = this.accessory.getService(this.hap.Service.TemperatureSensor) as Service; + accessory.removeService(this.TemperatureSensor.Service); } - this.temperatureService + } else { + accessory.context.TemperatureSensor = accessory.context.TemperatureSensor ?? {}; + this.TemperatureSensor = { + Name: accessory.context.TemperatureSensor.Name ?? `${accessory.displayName} Temperature Sensor`, + Service: accessory.getService(this.hap.Service.TemperatureSensor) ?? this.accessory.addService(this.hap.Service.TemperatureSensor) as Service, + CurrentTemperature: accessory.context.CurrentTemperature ?? 30, + }; + accessory.context.TemperatureSensor = this.TemperatureSensor as object; + + // Initialize Temperature Sensor Characteristics + this.TemperatureSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.TemperatureSensor.Name) .getCharacteristic(this.hap.Characteristic.CurrentTemperature) .setProps({ unit: Units['CELSIUS'], @@ -118,168 +104,122 @@ export class MeterPlus { minStep: 0.1, }) .onGet(() => { - return this.CurrentTemperature!; + return this.TemperatureSensor!.CurrentTemperature!; }); - } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Temperature Sensor Service Not Added`); } - - // Humidity Sensor Service + // Initialize Humidity Sensor Service if (device.meter?.hide_humidity) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Humidity Sensor Service`); - this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor); - accessory.removeService(this.humidityService!); - } else if (!this.humidityService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Humidity Sensor Service`); - const humidityService = `${accessory.displayName} Humidity Sensor`; - (this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor) - || this.accessory.addService(this.hap.Service.HumiditySensor)), humidityService; - - this.humidityService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Humidity Sensor`); - if (!this.humidityService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.humidityService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Humidity Sensor`); + if (this.HumiditySensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Humidity Sensor Service`); + this.HumiditySensor.Service = this.accessory.getService(this.hap.Service.HumiditySensor) as Service; + accessory.removeService(this.HumiditySensor.Service); } - this.humidityService + } else { + accessory.context.HumiditySensor = accessory.context.HumiditySensor ?? {}; + this.HumiditySensor = { + Name: accessory.context.HumiditySensor.Name ?? `${accessory.displayName} Humidity Sensor`, + Service: accessory.getService(this.hap.Service.HumiditySensor) ?? this.accessory.addService(this.hap.Service.HumiditySensor) as Service, + CurrentRelativeHumidity: accessory.context.CurrentRelativeHumidity ?? 50, + }; + accessory.context.HumiditySensor = this.HumiditySensor as object; + + // Initialize Humidity Sensor Characteristics + this.HumiditySensor!.Service + .setCharacteristic(this.hap.Characteristic.Name, this.HumiditySensor.Name) .getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity) .setProps({ minStep: 0.1, }) .onGet(() => { - return this.CurrentRelativeHumidity!; + return this.HumiditySensor!.CurrentRelativeHumidity!; }); - } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Humidity Sensor Service Not Added`); } - // Battery Service - const batteryService = `${accessory.displayName} Battery`; - (this.batteryService = this.accessory.getService(this.hap.Service.Battery) - || accessory.addService(this.hap.Service.Battery)), batteryService; - - this.batteryService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Battery`); - if (!this.batteryService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.batteryService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Battery`); - } - this.batteryService.setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE); + // Retrieve initial values and updateHomekit + this.refreshStatus(); // Retrieve initial values and updateHomekit this.updateHomeKitCharacteristics(); // Start an update interval interval(this.deviceRefreshRate * 1000) + .pipe(skipWhile(() => this.meterUpdateInProgress)) .subscribe(async () => { await this.refreshStatus(); }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - if (context.scale === 'CELSIUS') { - const { temperature, humidity } = context; - const { CurrentTemperature, CurrentRelativeHumidity } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(temperature, humidity) = ' + - `Webhook:(${temperature}, ${humidity}), ` + - `current:(${CurrentTemperature}, ${CurrentRelativeHumidity})`); - this.CurrentRelativeHumidity = humidity; - this.CurrentTemperature = temperature; - this.updateHomeKitCharacteristics(); - } - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); } - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // Battery - this.BatteryLevel = Number(this.BLE_BatteryLevel); - if (this.BatteryLevel < 15) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(serviceData.battery); + if (this.Battery.BatteryLevel < 15) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}, StatusLowBattery: ${this.StatusLowBattery}`); + this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}, StatusLowBattery: ${this.Battery.StatusLowBattery}`); // Humidity if (!this.device.meter?.hide_humidity) { - this.CurrentRelativeHumidity = this.BLE_CurrentRelativeHumidity!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.CurrentRelativeHumidity}%`); + this.HumiditySensor!.CurrentRelativeHumidity = serviceData.humidity!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.HumiditySensor!.CurrentRelativeHumidity}%`); } // Current Temperature if (!this.device.meter?.hide_temperature) { - this.BLE_Celsius < 0 ? 0 : this.BLE_Celsius > 100 ? 100 : this.BLE_Celsius; - this.CurrentTemperature = this.BLE_Celsius; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.CurrentTemperature}°c`); + serviceData.temperature!.c < 0 ? 0 : serviceData.temperature!.c > 100 ? 100 : serviceData.temperature!.c; + this.TemperatureSensor!.CurrentTemperature = serviceData.temperature!.c; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.TemperatureSensor!.CurrentTemperature}°c`); } } - async openAPIparseStatus(): Promise { + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); // BatteryLevel - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 15) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + } + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; } - this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}, StatusLowBattery: ${this.StatusLowBattery}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); // CurrentRelativeHumidity if (!this.device.meter?.hide_humidity) { - this.CurrentRelativeHumidity = this.OpenAPI_CurrentRelativeHumidity!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.CurrentRelativeHumidity}%`); + this.HumiditySensor!.CurrentRelativeHumidity = deviceStatus.body.humidity!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Humidity: ${this.HumiditySensor!.CurrentRelativeHumidity}%`); } // CurrentTemperature if (!this.device.meter?.hide_temperature) { - this.CurrentTemperature = this.OpenAPI_CurrentTemperature!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.CurrentTemperature}°c`); + this.TemperatureSensor!.CurrentTemperature = deviceStatus.body.temperature!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Temperature: ${this.TemperatureSensor!.CurrentTemperature}°c`); + } + + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); } - - // Battery - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; - } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - } - if (Number.isNaN(this.BatteryLevel)) { - this.BatteryLevel = 100; - } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` - + ` StatusLowBattery: ${this.StatusLowBattery}`); - - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; } /** @@ -294,10 +234,8 @@ export class MeterPlus { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -313,108 +251,44 @@ export class MeterPlus { this.getCustomBLEAddress(switchbot); // Start to monitor advertisement packets (async () => { - await switchbot.startScan({ - model: 'i', - id: this.device.bleMac, - }); + // Start to monitor advertisement packets + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === 'i') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_CurrentRelativeHumidity = ad.serviceData.humidity < 0 ? 0 : ad.serviceData.humidity > 100 ? 100 : ad.serviceData.humidity; - this.BLE_CurrentTemperature = ad.serviceData.temperature; - this.BLE_Celsius = ad.serviceData.temperature!.c; - this.BLE_Fahrenheit = ad.serviceData.temperature!.f; - this.BLE_BatteryLevel = ad.serviceData.battery; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: 'i', - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - if (ad.serviceData.humidity! > 0) { - // reject unreliable data - this.BLE_CurrentRelativeHumidity = ad.serviceData.humidity; - } - this.BLE_CurrentTemperature = ad.serviceData.temperature; - this.BLE_Celsius = ad.serviceData.temperature!.c; - this.BLE_Fahrenheit = ad.serviceData.temperature!.f; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} model: ${ad.serviceData.model}, modelName: ${ad.serviceData.modelName}, ` + - `temperature: ${JSON.stringify(ad.serviceData.temperature?.c)}, humidity: ${ad.serviceData.humidity}, ` + - `battery: ${ad.serviceData.battery}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_CurrentRelativeHumidity = deviceStatus.body.humidity!; - this.OpenAPI_CurrentTemperature = deviceStatus.body.temperature!; - this.OpenAPI_BatteryLevel = deviceStatus.body.battery; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -422,10 +296,37 @@ export class MeterPlus { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + if (context.scale === 'CELSIUS') { + const { temperature, humidity } = context; + const { CurrentRelativeHumidity } = this.HumiditySensor ?? { CurrentRelativeHumidity: undefined }; + const { CurrentTemperature } = this.TemperatureSensor ?? { CurrentTemperature: undefined }; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (temperature, humidity) = Webhook:(${temperature}, ${humidity}), ` + + `current:(${CurrentTemperature}, ${CurrentRelativeHumidity})`); + if (!device.meter?.hide_humidity) { + this.HumiditySensor!.CurrentRelativeHumidity = humidity; + } + if (!device.meter?.hide_temperature) { + this.TemperatureSensor!.CurrentTemperature = temperature; + } + this.updateHomeKitCharacteristics(); + } + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -438,67 +339,73 @@ export class MeterPlus { // CurrentRelativeHumidity if (!this.device.meter?.hide_humidity) { - if (this.CurrentRelativeHumidity === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + if (this.HumiditySensor!.CurrentRelativeHumidity === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}`); } else { + this.accessory.context.CurrentRelativeHumidity = this.HumiditySensor!.CurrentRelativeHumidity; + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, + this.HumiditySensor!.CurrentRelativeHumidity); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` updateCharacteristic CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}`); if (this.device.mqttURL) { - mqttmessage.push(`"humidity": ${this.CurrentRelativeHumidity}`); + mqttmessage.push(`"humidity": ${this.HumiditySensor!.CurrentRelativeHumidity}`); } if (this.device.history) { - entry['humidity'] = this.CurrentRelativeHumidity; + entry['humidity'] = this.HumiditySensor!.CurrentRelativeHumidity; } - this.accessory.context.CurrentRelativeHumidity = this.CurrentRelativeHumidity; - this.humidityService?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.CurrentRelativeHumidity); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` updateCharacteristic CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`, - ); } } + // CurrentTemperature if (!this.device.meter?.hide_temperature) { - if (this.CurrentTemperature === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}`); + if (this.TemperatureSensor!.CurrentTemperature === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); } else { + this.accessory.context.CurrentTemperature = this.TemperatureSensor!.CurrentTemperature; + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.TemperatureSensor!.CurrentTemperature); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); if (this.device.mqttURL) { - mqttmessage.push(`"temperature": ${this.CurrentTemperature}`); + mqttmessage.push(`"temperature": ${this.TemperatureSensor!.CurrentTemperature}`); } if (this.device.history) { - entry['temp'] = this.CurrentTemperature; + entry['temp'] = this.TemperatureSensor!.CurrentTemperature; } - this.accessory.context.CurrentTemperature = this.CurrentTemperature; - this.temperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.CurrentTemperature); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.CurrentTemperature}`); } } + // BatteryLevel - if (this.BatteryLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}`); + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); } else { + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery!.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); if (this.device.mqttURL) { - mqttmessage.push(`"battery": ${this.BatteryLevel}`); + mqttmessage.push(`"battery": ${this.Battery.BatteryLevel}`); } - this.accessory.context.BatteryLevel = this.BatteryLevel; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.BatteryLevel); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); } // StatusLowBattery - if (this.StatusLowBattery === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); } else { + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery!.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); if (this.device.mqttURL) { - mqttmessage.push(`"lowBattery": ${this.StatusLowBattery}`); + mqttmessage.push(`"lowBattery": ${this.Battery.StatusLowBattery}`); } - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); } - // MQTT + // MQTT Publish if (this.device.mqttURL) { this.mqttPublish(`{${mqttmessage.join(',')}}`); } - if (Number(this.CurrentRelativeHumidity) > 0) { + + // History Service + if (!this.device.meter?.hide_humidity && (Number(this.HumiditySensor!.CurrentRelativeHumidity) > 0)) { // reject unreliable data if (this.device.history) { this.historyService?.addEntry(entry); @@ -506,84 +413,6 @@ export class MeterPlus { } } - /* - * Publish MQTT message for topics of - * 'homebridge-switchbot/meter/xx:xx:xx:xx:xx:xx' - */ - mqttPublish(message: any) { - const mac = this.device.deviceId - ?.toLowerCase() - .match(/[\s\S]{1,2}/g) - ?.join(':'); - const options = this.device.mqttPubOptions || {}; - this.mqttClient?.publish(`homebridge-switchbot/meter/${mac}`, `${message}`, options); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT message: ${message} options:${JSON.stringify(options)}`); - } - - /* - * Setup MQTT hadler if URL is specified. - */ - async setupMqtt(device: device & devicesConfig): Promise { - if (device.mqttURL) { - try { - const { connectAsync } = asyncmqtt; - this.mqttClient = await connectAsync(device.mqttURL, device.mqttOptions || {}); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MQTT connection has been established successfully.`); - this.mqttClient.on('error', (e: Error) => { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to publish MQTT messages. ${e}`); - }); - } catch (e) { - this.mqttClient = null; - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Failed to establish MQTT connection. ${e}`); - } - } - } - - /* - * Setup EVE history graph feature if enabled. - */ - async setupHistoryService(device: device & devicesConfig): Promise { - const mac = this.device - .deviceId!.match(/.{1,2}/g)! - .join(':') - .toLowerCase(); - this.historyService = device.history - ? new this.platform.fakegatoAPI('room', this.accessory, { - log: this.platform.log, - storage: 'fs', - filename: `${hostname().split('.')[0]}_${mac}_persist.json`, - }) - : null; - } - - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); - } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 'i', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); - } - } - async BLERefreshConnection(switchbot: any): Promise { this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} wasn't able to establish BLE Connection, node-switchbot:` + ` ${JSON.stringify(switchbot)}`); @@ -593,259 +422,26 @@ export class MeterPlus { } } - model(device: device & devicesConfig): CharacteristicValue { - let model: string; - if (device.deviceType === 'Meter Plus (JP)') { - model = 'W2201500'; - } else { - model = 'W2301500'; - } - return model; - } - - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); + if (!this.device.meter?.hide_humidity) { + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, 50); + } + if (!this.device.meter?.hide_temperature) { + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, 30); + } + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, 100); } } async apiError(e: any): Promise { if (!this.device.meter?.hide_humidity) { - this.humidityService?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); + this.HumiditySensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); } if (!this.device.meter?.hide_temperature) { - this.temperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); - } - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); - } - - async deviceContext() { - if (this.CurrentRelativeHumidity === undefined) { - this.CurrentRelativeHumidity = 0; - } else { - this.CurrentRelativeHumidity = this.accessory.context.CurrentRelativeHumidity; - } - if (this.CurrentTemperature === undefined) { - this.CurrentTemperature = 0; - } else { - this.CurrentTemperature = this.accessory.context.CurrentTemperature; - } - if (this.BatteryLevel === undefined) { - this.BatteryLevel = 100; - } else { - this.BatteryLevel = this.accessory.context.BatteryLevel; - } - if (this.StatusLowBattery === undefined) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - } else { - this.StatusLowBattery = this.accessory.context.StatusLowBattery; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.meter) { - config = device.meter; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.mqttURL !== undefined) { - config['mqttURL'] = device.mqttURL; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); + this.TemperatureSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.Battery!.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); + this.Battery!.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); } } diff --git a/src/device/motion.ts b/src/device/motion.ts index 18d495b2..d59f83f5 100644 --- a/src/device/motion.ts +++ b/src/device/motion.ts @@ -1,137 +1,130 @@ -import { request } from 'undici'; -import { sleep } from '../utils.js'; -import { interval, Subject } from 'rxjs'; -import { skipWhile } from 'rxjs/operators'; -import { SwitchBotPlatform } from '../platform.js'; -import { Service, PlatformAccessory, CharacteristicValue, API, Logging, HAP } from 'homebridge'; -import { device, devicesConfig, serviceData, deviceStatus, Devices, SwitchBotPlatformConfig } from '../settings.js'; + +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * motion.ts: @switchbot/homebridge-switchbot. + */ +import { Devices } from '../settings.js'; +import { deviceBase } from './device.js'; +import { Subject, interval, skipWhile } from 'rxjs'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class Motion { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class Motion extends deviceBase { // Services - batteryService: Service; - motionSensorService!: Service; - lightSensorService?: Service; - - // Characteristic Values - BatteryLevel!: CharacteristicValue; - MotionDetected!: CharacteristicValue; - StatusLowBattery!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - CurrentAmbientLightLevel!: CharacteristicValue; - - // OpenAPI Others - OpenAPI_BatteryLevel: deviceStatus['battery']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - OpenAPI_MotionDetected: deviceStatus['moveDetected']; - OpenAPI_CurrentAmbientLightLevel: deviceStatus['brightness']; - - // Status - BLE_BatteryLevel!: serviceData['battery']; - BLE_MotionDetected!: serviceData['movement']; - BLE_CurrentAmbientLightLevel!: serviceData['lightLevel']; - - // BLE Others - scanning!: boolean; - BLE_IsConnected?: boolean; - - // Config - set_minLux!: number; - set_maxLux!: number; - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; + private Battery: { + Name: CharacteristicValue; + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + }; + + private MotionSensor: { + Name: CharacteristicValue; + Service: Service; + MotionDetected: CharacteristicValue; + }; + + private LightSensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentAmbientLightLevel: CharacteristicValue; + }; // Updates motionUbpdateInProgress!: boolean; doMotionUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); - this.deviceContext(); - this.deviceConfig(device); - + super(platform, accessory, device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doMotionUpdate = new Subject(); this.motionUbpdateInProgress = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, 'W1101500') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Battery service if it exists, otherwise create a new Motion service - // you can create multiple services for each accessory - const motionSensorService = `${accessory.displayName} Motion Sensor`; - (this.motionSensorService = accessory.getService(this.hap.Service.MotionSensor) - || accessory.addService(this.hap.Service.MotionSensor)), motionSensorService; + // Initialize Motion Sensor property + accessory.context.MotionSensor = accessory.context.MotionSensor ?? {}; + this.MotionSensor = { + Name: accessory.context.MotionSensor.Name ?? `${accessory.displayName} Motion Sensor`, + Service: accessory.getService(this.hap.Service.MotionSensor) ?? accessory.addService(this.hap.Service.MotionSensor) as Service, + MotionDetected: accessory.context.MotionDetected ?? false, + }; + accessory.context.MotionSensor = this.MotionSensor as object; + + // Initialize Motion Sensor Characteristics + this.MotionSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .setCharacteristic(this.hap.Characteristic.StatusActive, true) + .getCharacteristic(this.hap.Characteristic.MotionDetected) + .onGet(() => { + return this.MotionSensor.MotionDetected; + }); + accessory.context.MotionSensorName = this.MotionSensor.Name; + + // Initialize Battery Service + accessory.context.Battery = accessory.context.Battery ?? {}; + this.Battery = { + Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Characteristics + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE) + .getCharacteristic(this.hap.Characteristic.BatteryLevel) + .onGet(() => { + return this.Battery.BatteryLevel; + }); - this.motionSensorService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.motionSensorService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.motionSensorService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/MotionSensor + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery) + .getCharacteristic(this.hap.Characteristic.StatusLowBattery) + .onGet(() => { + return this.Battery.StatusLowBattery; + }); - // Light Sensor Service + // Initialize Light Sensor Service if (device.motion?.hide_lightsensor) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Light Sensor Service`); - this.lightSensorService = this.accessory.getService(this.hap.Service.LightSensor); - accessory.removeService(this.lightSensorService!); - } else if (!this.lightSensorService) { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Add Light Sensor Service`); - const lightSensorService = `${accessory.displayName} Light Sensor`; - (this.lightSensorService = this.accessory.getService(this.hap.Service.LightSensor) - || this.accessory.addService(this.hap.Service.LightSensor)), lightSensorService; - - this.lightSensorService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Light Sensor`); - this.lightSensorService.setCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Light Sensor`); + if (this.LightSensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Light Sensor Service`); + this.LightSensor.Service = this.accessory.getService(this.hap.Service.LightSensor) as Service; + accessory.removeService(this.LightSensor.Service); + } } else { - this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Light Sensor Service Not Added`); - } + accessory.context.LightSensor = accessory.context.LightSensor ?? {}; + this.LightSensor = { + Name: accessory.context.LightSensor.Name ?? `${accessory.displayName} Light Sensor`, + Service: accessory.getService(this.hap.Service.LightSensor) ?? this.accessory.addService(this.hap.Service.LightSensor) as Service, + CurrentAmbientLightLevel: accessory.context.CurrentAmbientLightLevel ?? 0.0001, + }; + accessory.context.LightSensor = this.LightSensor as object; + + // Initialize LightSensor Characteristics + this.LightSensor!.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LightSensor!.Name) + .setCharacteristic(this.hap.Characteristic.StatusActive, true) + .getCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel) + .onGet(() => { + return this.LightSensor!.CurrentAmbientLightLevel; + }); + }; - // Battery Service - const batteryService = `${accessory.displayName} Battery`; - (this.batteryService = this.accessory.getService(this.hap.Service.Battery) - || accessory.addService(this.hap.Service.Battery)), batteryService; - this.batteryService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Battery`); - if (!this.batteryService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.batteryService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Battery`); - } - this.batteryService.setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE); + // Retrieve initial values and updateHomekit + this.refreshStatus(); // Retrieve initial values and updateHomekit this.updateHomeKitCharacteristics(); @@ -144,124 +137,98 @@ export class Motion { }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - const { detectionState } = context; - const { MotionDetected } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(detectionState) = ' + - `Webhook:(${detectionState}), ` + - `current:(${MotionDetected})`); - this.MotionDetected = detectionState === 'DETECTED' ? true : false; - this.updateHomeKitCharacteristics(); - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); } - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // Movement - this.MotionDetected = this.BLE_MotionDetected!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionDetected}`); - if (this.MotionDetected !== this.accessory.context.MotionDetected && this.MotionDetected) { + this.MotionSensor.MotionDetected = serviceData.movement!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionSensor.MotionDetected}`); + if (this.MotionSensor.MotionDetected !== this.accessory.context.MotionDetected && this.MotionSensor.MotionDetected) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Detected Motion`); } // Light Level if (!this.device.motion?.hide_lightsensor) { - this.set_minLux = this.minLux(); - this.set_maxLux = this.maxLux(); - switch (this.BLE_CurrentAmbientLightLevel) { + const set_minLux = this.device.motion?.set_minLux ?? 1; + const set_maxLux = this.device.motion?.set_maxLux ?? 6001; + switch (serviceData.lightLevel) { case 'dark': case 1: - this.CurrentAmbientLightLevel = this.set_minLux; + this.LightSensor!.CurrentAmbientLightLevel = set_minLux; break; default: - this.CurrentAmbientLightLevel = this.set_maxLux; + this.LightSensor!.CurrentAmbientLightLevel = set_maxLux; } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${this.BLE_CurrentAmbientLightLevel},` + - ` CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`, - ); - if (this.CurrentAmbientLightLevel !== this.accessory.context.CurrentAmbientLightLevel) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LightLevel: ${serviceData.lightLevel},` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); + if (this.LightSensor!.CurrentAmbientLightLevel !== this.accessory.context.CurrentAmbientLightLevel) { + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } } // Battery - if (this.BLE_BatteryLevel === undefined) { - this.BLE_BatteryLevel = 100; + if (serviceData.battery === undefined) { + serviceData.battery = 100; } - this.BatteryLevel = this.BLE_BatteryLevel!; - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = serviceData.battery!; + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` + ` StatusLowBattery: ${this.StatusLowBattery}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); } - async openAPIparseStatus(): Promise { + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); // Motion State - this.MotionDetected = this.OpenAPI_MotionDetected!; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionDetected}`); + this.MotionSensor.MotionDetected = deviceStatus.body.moveDetected!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionSensor.MotionDetected}`); // Light Level if (!this.device.motion?.hide_lightsensor) { - this.set_minLux = this.minLux(); - this.set_maxLux = this.maxLux(); - switch (this.OpenAPI_CurrentAmbientLightLevel) { + const set_minLux = this.device.motion?.set_minLux ?? 1; + const set_maxLux = this.device.motion?.set_maxLux ?? 6001; + switch (deviceStatus.body.brightness) { case 'dim': - this.CurrentAmbientLightLevel = this.set_minLux; + this.LightSensor!.CurrentAmbientLightLevel = set_minLux; break; case 'bright': default: - this.CurrentAmbientLightLevel = this.set_maxLux; + this.LightSensor!.CurrentAmbientLightLevel = set_maxLux; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } // Battery - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 10) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - } - if (Number.isNaN(this.BatteryLevel)) { - this.BatteryLevel = 100; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + } + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; + } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); + + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` - + ` StatusLowBattery: ${this.StatusLowBattery}`); - - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; } /** @@ -276,10 +243,8 @@ export class Motion { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -287,126 +252,52 @@ export class Motion { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLERefreshStatus`); const switchbot = await this.platform.connectBLE(); // Convert to BLE Address - this.device.bleMac = - this.device.customBLEaddress || - this.device - .deviceId!.match(/.{1,2}/g)! - .join(':') - .toLowerCase(); + this.device.bleMac = this.device + .deviceId!.match(/.{1,2}/g)! + .join(':') + .toLowerCase(); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE Address: ${this.device.bleMac}`); this.getCustomBLEAddress(switchbot); // Start to monitor advertisement packets (async () => { // Start to monitor advertisement packets - await switchbot.startScan({ - model: 's', - id: this.device.bleMac, - }); + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === 's') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_MotionDetected = ad.serviceData.movement; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.BLE_CurrentAmbientLightLevel = ad.serviceData.lightLevel; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - await switchbot - .startScan({ - model: 's', - id: this.device.bleMac, - }) - .then(async () => { - return await this.retry({ - max: this.maxRetry(), - fn: async () => { - // Set an event handler - this.scanning = true; - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.BLE_MotionDetected = ad.serviceData.movement; - this.BLE_BatteryLevel = ad.serviceData.battery; - this.BLE_CurrentAmbientLightLevel = ad.serviceData.lightLevel; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} movement: ${ad.serviceData.movement},` - + ` lightLevel: ${ad.serviceData.lightLevel}, battery: ${ad.serviceData.battery}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - this.debugErrorLog('1'); - await this.stopScanning(switchbot); - this.scanning = false; - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} scanning: ${this.scanning}`); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }, - }); - }) - .then(async () => { - // Stop to monitor - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} scanning: ${this.scanning}`); - if (this.scanning) { - this.debugErrorLog('2'); - await this.stopScanning(switchbot); - } - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - this.debugErrorLog('3'); - await this.BLERefreshConnection(switchbot); - }); - } else { - this.debugErrorLog('4'); + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_MotionDetected = deviceStatus.body.moveDetected; - this.OpenAPI_CurrentAmbientLightLevel = deviceStatus.body.brightness; - this.OpenAPI_BatteryLevel = deviceStatus.body.battery; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -414,10 +305,29 @@ export class Motion { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { detectionState } = context; + const { MotionDetected } = this.MotionSensor; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (detectionState) = Webhook: (${detectionState}),` + + ` current: (${MotionDetected})`); + this.MotionSensor.MotionDetected = detectionState === 'DETECTED' ? true : false; + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -425,69 +335,42 @@ export class Motion { * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics(): Promise { - if (this.MotionDetected === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionDetected}`); + if (this.MotionSensor.MotionDetected === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} MotionDetected: ${this.MotionSensor.MotionDetected}`); } else { - this.accessory.context.MotionDetected = this.MotionDetected; - this.motionSensorService.updateCharacteristic(this.hap.Characteristic.MotionDetected, this.MotionDetected); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic MotionDetected: ${this.MotionDetected}`); + this.accessory.context.MotionDetected = this.MotionSensor.MotionDetected; + this.MotionSensor.Service.updateCharacteristic(this.hap.Characteristic.MotionDetected, this.MotionSensor.MotionDetected); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` MotionDetected: ${this.MotionSensor.MotionDetected}`); } - if (this.BatteryLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}`); + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); } else { - this.accessory.context.BatteryLevel = this.BatteryLevel; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.BatteryLevel); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); } - if (this.StatusLowBattery === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); } else { - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); } if (this.BLE) { - if (this.CurrentAmbientLightLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`); + if (this.LightSensor!.CurrentAmbientLightLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } else { - this.accessory.context.CurrentAmbientLightLevel = this.CurrentAmbientLightLevel; - this.lightSensorService?.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.CurrentAmbientLightLevel); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName}` + - ` updateCharacteristic CurrentAmbientLightLevel: ${this.CurrentAmbientLightLevel}`, - ); + this.accessory.context.CurrentAmbientLightLevel = this.LightSensor!.CurrentAmbientLightLevel; + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, this.LightSensor!.CurrentAmbientLightLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName}` + + ` updateCharacteristic CurrentAmbientLightLevel: ${this.LightSensor!.CurrentAmbientLightLevel}`); } } } - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); - } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: 's', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); - } - } - async BLERefreshConnection(switchbot: any): Promise { this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} wasn't able to establish BLE Connection, node-switchbot:` + ` ${JSON.stringify(switchbot)}`); @@ -497,269 +380,18 @@ export class Motion { } } - async retry({ max, fn }: { max: number; fn: { (): any; (): Promise } }): Promise { - return fn().catch(async (e: any) => { - if (max === 0) { - throw e; - } - this.infoLog(e); - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Retrying`); - await sleep(1000); - return this.retry({ max: max - 1, fn }); - }); - } - - maxRetry(): number { - if (this.device.maxRetry) { - return this.device.maxRetry; - } else { - return 5; - } - } - - minLux(): number { - if (this.device.motion?.set_minLux) { - this.set_minLux = this.device.motion!.set_minLux!; - } else { - this.set_minLux = 1; - } - return this.set_minLux; - } - - maxLux(): number { - if (this.device.motion?.set_maxLux) { - this.set_maxLux = this.device.motion!.set_maxLux!; - } else { - this.set_maxLux = 6001; - } - return this.set_maxLux; - } - - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); + this.MotionSensor.Service.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); } } async apiError(e: any): Promise { - this.motionSensorService.updateCharacteristic(this.hap.Characteristic.MotionDetected, e); - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); + this.MotionSensor.Service.updateCharacteristic(this.hap.Characteristic.MotionDetected, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); if (this.BLE) { - this.lightSensorService?.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, e); - } - } - - async deviceContext() { - if (this.MotionDetected === undefined) { - this.MotionDetected = false; - } else { - this.MotionDetected = this.accessory.context.MotionDetected; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.motion) { - config = device.motion; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; + this.LightSensor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentAmbientLightLevel, e); } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.offline !== undefined) { - config['offline'] = device.offline; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; } } diff --git a/src/device/plug.ts b/src/device/plug.ts index e3e9e9ae..f7dca7e3 100644 --- a/src/device/plug.ts +++ b/src/device/plug.ts @@ -1,94 +1,58 @@ +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * plug.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { sleep } from '../utils.js'; -import { interval, Subject } from 'rxjs'; -import { SwitchBotPlatform } from '../platform.js'; -import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { Service, PlatformAccessory, CharacteristicValue, API, Logging, HAP } from 'homebridge'; -import { device, devicesConfig, deviceStatus, serviceData, Devices, SwitchBotPlatformConfig } from '../settings.js'; - -export class Plug { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +import { Devices } from '../settings.js'; +import { deviceBase } from './device.js'; +import { Subject, debounceTime, interval, skipWhile, take, tap } from 'rxjs'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; +export class Plug extends deviceBase { // Services - outletService: Service; - - // Characteristic Values - On!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - - // OpenAPI Others - OpenAPI_On: deviceStatus['power']; - OpenAPI_FirmwareRevision: deviceStatus['version']; - - // BLE Others - BLE_IsConnected?: boolean; - BLE_On: serviceData['state']; - - // Config - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; + private Outlet: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; // Updates plugUpdateInProgress!: boolean; doPlugUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); - this.deviceContext(); - this.deviceConfig(device); - + super(platform, accessory, device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doPlugUpdate = new Subject(); this.plugUpdateInProgress = false; + // Initialize Outlet Service + accessory.context.Outlet = accessory.context.Outlet ?? {}; + this.Outlet = { + Name: accessory.context.Outlet.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Outlet) ?? accessory.addService(this.hap.Service.Outlet) as Service, + On: accessory.context.On || false, + }; + accessory.context.Outlet = this.Outlet as object; + + // Initialize Outlet Characteristics + this.Outlet.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.Outlet.On; + }) + .onSet(this.OnSet.bind(this)); + // Retrieve initial values and updateHomekit this.refreshStatus(); - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, this.model(device)) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Outlet service if it exists, otherwise create a new Outlet service - // you can create multiple services for each accessory - const outletService = `${accessory.displayName} ${device.deviceType}`; - (this.outletService = accessory.getService(this.hap.Service.Outlet) - || accessory.addService(this.hap.Service.Outlet)), outletService; - - this.outletService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.outletService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.outletService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/Outlet - - // create handlers for required characteristics - this.outletService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); - // Update Homekit this.updateHomeKitCharacteristics(); @@ -100,25 +64,7 @@ export class Plug { }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - const { powerState } = context; - const { On } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(powerState) = ' + - `Webhook:(${powerState}), ` + - `current:(${On})`); - this.On = powerState === 'ON' ? true : false; - this.updateHomeKitCharacteristics(); - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); // Watch for Plug change events // We put in a debounce of 100ms so we don't make duplicate calls @@ -127,67 +73,58 @@ export class Plug { tap(() => { this.plugUpdateInProgress = true; }), - debounceTime(this.platform.config.options!.pushRate! * 1000), + debounceTime(this.devicePushRate * 1000), ) .subscribe(async () => { try { await this.pushChanges(); } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + - ` Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${device.connectionType} Connection,` + + ` Error Message: ${JSON.stringify(e.message)}`); } this.plugUpdateInProgress = false; }); } - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - } else if (this.BLE) { - await this.BLEparseStatus(); - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); - } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); - } - } - - async BLEparseStatus(): Promise { + async BLEparseStatus(serviceData: serviceData): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // State - switch (this.BLE_On) { + switch (serviceData.state) { case 'on': - this.On = true; + this.Outlet.On = true; break; default: - this.On = false; + this.Outlet.On = false; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Outlet.On}`); } - async openAPIparseStatus() { + async openAPIparseStatus(deviceStatus: deviceStatus) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); - switch (this.OpenAPI_On) { + switch (deviceStatus.body.power) { case 'on': - this.On = true; + this.Outlet.On = true; break; default: - this.On = false; + this.Outlet.On = false; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Outlet.On}`); - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); + } } /** @@ -202,10 +139,8 @@ export class Plug { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -221,94 +156,44 @@ export class Plug { this.getCustomBLEAddress(switchbot); // Start to monitor advertisement packets (async () => { - await switchbot.startScan({ - model: this.BLEmodel(), - id: this.device.bleMac, - }); + // Start to monitor advertisement packets + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === this.BLEmodel()) { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_On = ad.serviceData.state; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: this.BLEmodel(), - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.BLE_On = ad.serviceData.state; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} state: ${ad.serviceData.state}, ` + - `delay: ${ad.serviceData.delay}, timer: ${ad.serviceData.timer}, syncUtcTime: ${ad.serviceData.syncUtcTime} ` + - `wifiRssi: ${ad.serviceData.wifiRssi}, overload: ${ad.serviceData.overload}, currentPower: ${ad.serviceData.currentPower}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } - async openAPIRefreshStatus() { + async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_On = deviceStatus.body.power; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -316,10 +201,28 @@ export class Plug { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { powerState } = context; + const { On } = this.Outlet; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (powerState) = Webhook: (${powerState}), current:(${On})`); + this.Outlet.On = powerState === 'ON' ? true : false; + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -342,9 +245,8 @@ export class Plug { await this.openAPIpushChanges(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, pushChanges will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, pushChanges will not happen.`); } // Refresh the status from the API interval(15000) @@ -357,8 +259,9 @@ export class Plug { async BLEpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`); - if (this.On !== this.accessory.context.On) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges On: ${this.On} OnCached: ${this.accessory.context.On}`); + if (this.Outlet.On !== this.accessory.context.On) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges` + + ` On: ${this.Outlet.On} OnCached: ${this.accessory.context.On}`); const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -368,15 +271,15 @@ export class Plug { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE Address: ${this.device.bleMac}`); switchbot .discover({ - model: this.BLEmodel(), + model: this.device.bleModel, id: this.device.bleMac, }) .then(async (device_list: any) => { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); - return await this.retry({ - max: this.maxRetry(), + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Outlet.On}`); + return await this.retryBLE({ + max: await this.maxRetryBLE(), fn: async () => { - if (this.On) { + if (this.Outlet.On) { return await device_list[0].turnOn({ id: this.device.bleMac }); } else { return await device_list[0].turnOff({ id: this.device.bleMac }); @@ -386,28 +289,27 @@ export class Plug { }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.On = false; + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `On: ${this.Outlet.On} sent over BLE, sent successfully`); + this.Outlet.On = false; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges.` + `On: ${this.On}, ` + `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges,` + + ` On: ${this.Outlet.On}, OnCached: ${this.accessory.context.On}`); } } async openAPIpushChanges() { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`); - if (this.On !== this.accessory.context.On) { + if (this.Outlet.On !== this.accessory.context.On) { let command = ''; - if (this.On) { + if (this.Outlet.On) { command = 'turnOn'; } else { command = 'turnOff'; @@ -432,23 +334,20 @@ export class Plug { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.` + - `On: ${this.On}, ` + - `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.` + + `On: ${this.Outlet.On}, OnCached: ${this.accessory.context.On}`); } } @@ -456,60 +355,24 @@ export class Plug { * Handle requests to set the value of the "On" characteristic */ async OnSet(value: CharacteristicValue): Promise { - if (this.On === this.accessory.context.On) { + if (this.Outlet.On === this.accessory.context.On) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set On: ${value}`); } else { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set On: ${value}`); } - this.On = value; + this.Outlet.On = value; this.doPlugUpdate.next(); } async updateHomeKitCharacteristics(): Promise { // On - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); - } else { - this.accessory.context.On = this.On; - this.outletService.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); - } - } - - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); + if (this.Outlet.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.Outlet.On}`); } else { - await this.BLERefreshConnection(switchbot); - } - } - - BLEmodel(): 'g' | 'j' { - if (this.device.deviceType === 'Plug Mini (US)' || this.device.configDeviceType === 'Plug Mini (US)') { - return 'g'; - } else { - return 'j'; - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: this.BLEmodel(), - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); + this.accessory.context.On = this.Outlet.On; + this.Outlet.Service.updateCharacteristic(this.hap.Characteristic.On, this.Outlet.On); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.Outlet.On}`); } } @@ -529,262 +392,13 @@ export class Plug { } } - async retry({ max, fn }: { max: number; fn: { (): any; (): Promise } }): Promise { - return fn().catch(async (e: any) => { - if (max === 0) { - throw e; - } - this.infoLog(e); - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Retrying`); - await sleep(1000); - return this.retry({ max: max - 1, fn }); - }); - } - - maxRetry(): number { - if (this.device.maxRetry) { - return this.device.maxRetry; - } else { - return 5; - } - } - - model(device: device & devicesConfig): string { - let model: string; - if (device.deviceType === 'Plug Mini (US)') { - model = 'W1901400'; - model = 'W1901401'; - } else if (device.deviceType === 'Plug Mini (JP)') { - model = 'W2001400'; - } else { - model = 'SP11'; - } - return model; - } - - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); + this.Outlet.Service.updateCharacteristic(this.hap.Characteristic.On, false); } } async apiError(e: any): Promise { - this.outletService.updateCharacteristic(this.hap.Characteristic.On, e); - } - - async deviceContext() { - if (this.On === undefined) { - this.On = false; - } else { - this.On = this.accessory.context.On; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.plug) { - config = device.plug; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.offline !== undefined) { - config['offline'] = device.offline; - } - if (device.maxRetry !== undefined) { - config['maxRetry'] = device.maxRetry; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.Outlet.Service.updateCharacteristic(this.hap.Characteristic.On, e); } } diff --git a/src/device/robotvacuumcleaner.ts b/src/device/robotvacuumcleaner.ts index 056cfa19..972e1f35 100644 --- a/src/device/robotvacuumcleaner.ts +++ b/src/device/robotvacuumcleaner.ts @@ -1,103 +1,69 @@ +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * robotvacuumcleaner.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { sleep } from '../utils.js'; -import { interval, Subject } from 'rxjs'; -import { SwitchBotPlatform } from '../platform.js'; -import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { Service, PlatformAccessory, CharacteristicValue, API, Logging, HAP } from 'homebridge'; -import { device, devicesConfig, deviceStatus, serviceData, Devices, SwitchBotPlatformConfig } from '../settings.js'; - -export class RobotVacuumCleaner { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; - // Services - batteryService: Service; - robotVacuumCleanerService: Service; - - // Characteristic Values - On!: CharacteristicValue; - Brightness!: CharacteristicValue; - BatteryLevel!: CharacteristicValue; - StatusLowBattery!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - - // OpenAPI Status - OpenAPI_On: deviceStatus['power']; - OpenAPI_BatteryLevel: deviceStatus['battery']; - OpenAPI_FirmwareRevision: deviceStatus['version']; +import { Devices } from '../settings.js'; +import { deviceBase } from './device.js'; +import { Subject, interval, skipWhile } from 'rxjs'; +import { debounceTime, take, tap } from 'rxjs/operators'; - // BLE Status - BLE_On: serviceData['state']; +import type { SwitchBotPlatform } from '../platform.js'; +import type { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; +import type { device, devicesConfig, deviceStatus, serviceData } from '../settings.js'; - // BLE Others - BLE_IsConnected?: boolean; - - // Config - scanDuration!: number; - deviceLogging!: string; - deviceRefreshRate!: number; +export class RobotVacuumCleaner extends deviceBase { + // Services + private LightBulb: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + Brightness: CharacteristicValue; + }; + + private Battery: { + Name: CharacteristicValue; + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + ChargingState: CharacteristicValue; + }; // Updates robotVacuumCleanerUpdateInProgress!: boolean; doRobotVacuumCleanerUpdate!: Subject; - // Connection - private readonly OpenAPI: boolean; - private readonly BLE: boolean; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: device & devicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // Connection - this.BLE = this.device.connectionType === 'BLE' || this.device.connectionType === 'BLE/OpenAPI'; - this.OpenAPI = this.device.connectionType === 'OpenAPI' || this.device.connectionType === 'BLE/OpenAPI'; - // default placeholders - this.deviceLogs(device); - this.scan(device); - this.refreshRate(device); - this.deviceContext(); - this.deviceConfig(device); - + super(platform, accessory, device); // this is subject we use to track when we need to POST changes to the SwitchBot API this.doRobotVacuumCleanerUpdate = new Subject(); this.robotVacuumCleanerUpdateInProgress = false; - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, this.model(device)) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Lightbulb service if it exists, otherwise create a new Lightbulb service - // you can create multiple services for each accessory - const robotVacuumCleanerService = `${accessory.displayName} ${device.deviceType}`; - (this.robotVacuumCleanerService = accessory.getService(this.hap.Service.Lightbulb) - || accessory.addService(this.hap.Service.Lightbulb)), robotVacuumCleanerService; - - this.robotVacuumCleanerService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.robotVacuumCleanerService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.robotVacuumCleanerService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/Lightbulb - - // create handlers for required characteristics - this.robotVacuumCleanerService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); + // Initialize Lightbulb Service + accessory.context.LightBulb = accessory.context.LightBulb ?? {}; + this.LightBulb = { + Name: accessory.context.LightBulb.Name ?? accessory.displayName, + Service: accessory.getService(this.hap.Service.Lightbulb) ?? accessory.addService(this.hap.Service.Lightbulb) as Service, + On: accessory.context.On ?? false, + Brightness: accessory.context.Brightness ?? 0, + }; + accessory.context.LightBulb = this.LightBulb as object; + + // Initialize LightBulb Characteristics + this.LightBulb.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.LightBulb.On; + }) + .onSet(this.OnSet.bind(this)); - // handle Brightness events using the Brightness characteristic - this.robotVacuumCleanerService + // Initialize LightBulb Brightness Characteristic + this.LightBulb.Service .getCharacteristic(this.hap.Characteristic.Brightness) .setProps({ minStep: 25, @@ -107,20 +73,43 @@ export class RobotVacuumCleaner { validValueRanges: [0, 100], }) .onGet(() => { - return this.Brightness; + return this.LightBulb.Brightness; }) .onSet(this.BrightnessSet.bind(this)); - // Battery Service - const batteryService = `${accessory.displayName} Battery`; - (this.batteryService = this.accessory.getService(this.hap.Service.Battery) - || accessory.addService(this.hap.Service.Battery)), batteryService; + // Initialize Battery Service + accessory.context.Battery = accessory.context.Battery ?? {}; + this.Battery = { + Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + ChargingState: accessory.context.ChargingState ?? this.hap.Characteristic.ChargingState.NOT_CHARGING, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Characteristics + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .getCharacteristic(this.hap.Characteristic.BatteryLevel) + .onGet(() => { + return this.Battery.BatteryLevel; + }); - this.batteryService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Battery`); - if (!this.batteryService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.batteryService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Battery`); - } + this.Battery.Service + .getCharacteristic(this.hap.Characteristic.StatusLowBattery) + .onGet(() => { + return this.Battery.StatusLowBattery; + }); + this.Battery.Service + .getCharacteristic(this.hap.Characteristic.ChargingState) + .onGet(() => { + return this.Battery.ChargingState; + }); + + // Retrieve initial values and updateHomekit + this.refreshStatus(); // Update Homekit this.updateHomeKitCharacteristics(); @@ -132,26 +121,7 @@ export class RobotVacuumCleaner { }); //regisiter webhook event handler - if (this.device.webhook) { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} is listening webhook.`); - this.platform.webhookEventHandler[this.device.deviceId] = async (context) => { - try { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} received Webhook: ${JSON.stringify(context)}`); - const { onlineStatus, battery } = context; - const { On, BatteryLevel } = this; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + - '(onlineStatus, battery) = ' + - `Webhook:(${onlineStatus}, ${battery}), ` + - `current:(${On}, ${BatteryLevel})`); - this.On = onlineStatus === 'online' ? true : false; - this.BatteryLevel = battery; - this.updateHomeKitCharacteristics(); - } catch (e: any) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` - + `failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); - } - }; - } + this.registerWebhook(accessory, device); // Watch for Plug change events // We put in a debounce of 100ms so we don't make duplicate calls @@ -160,81 +130,91 @@ export class RobotVacuumCleaner { tap(() => { this.robotVacuumCleanerUpdateInProgress = true; }), - debounceTime(this.platform.config.options!.pushRate! * 1000), + debounceTime(this.devicePushRate * 1000), ) .subscribe(async () => { try { - if (this.On !== this.accessory.context.On) { + if (this.LightBulb.On !== accessory.context.On) { await this.pushChanges(); } - if (this.On && this.Brightness !== this.accessory.context.Brightness) { + if (this.LightBulb.On && this.LightBulb.Brightness !== accessory.context.Brightness) { await this.openAPIpushBrightnessChanges(); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType} Connection,` + - ` Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed pushChanges with ${device.connectionType} Connection,` + + ` Error Message: ${JSON.stringify(e.message)}`); } this.robotVacuumCleanerUpdateInProgress = false; }); } - /** - * Parse the device status from the SwitchBot api - */ - async parseStatus(): Promise { - if (!this.device.enableCloudService && this.OpenAPI) { - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} parseStatus enableCloudService: ${this.device.enableCloudService}`); - /*} else if (this.BLE) { - await this.BLEparseStatus();*/ - } else if (this.OpenAPI && this.platform.config.credentials?.token) { - await this.openAPIparseStatus(); + async BLEparseStatus(serviceData: serviceData): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); + + // Battery + this.Battery.BatteryLevel = Number(serviceData.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, parseStatus will not happen.`, - ); + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - } + this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}, StatusLowBattery: ${this.Battery.StatusLowBattery}`); - async BLEparseStatus(): Promise { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); // State - switch (this.BLE_On) { + switch (serviceData.state) { case 'on': - this.On = true; + this.LightBulb.On = true; break; default: - this.On = false; + this.LightBulb.On = false; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); } - async openAPIparseStatus() { + async openAPIparseStatus(deviceStatus: deviceStatus) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); - switch (this.OpenAPI_On) { + switch (deviceStatus.body.power) { case 'on': - this.On = true; + this.LightBulb.On = true; break; default: - this.On = false; + this.LightBulb.On = false; } - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); - // Battery - this.BatteryLevel = Number(this.OpenAPI_BatteryLevel); - if (this.BatteryLevel < 15) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + // BatteryLevel + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + + // StatusLowBattery + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}, StatusLowBattery: ${this.StatusLowBattery}`); + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; + } + // ChargingState + this.Battery.ChargingState = deviceStatus.body.workingStatus === 'Charging' + ? this.hap.Characteristic.ChargingState.CHARGING : this.hap.Characteristic.ChargingState.NOT_CHARGING; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}, ChargingState: ${this.Battery.ChargingState}`); - // FirmwareRevision - this.FirmwareRevision = this.OpenAPI_FirmwareRevision!; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); + } } /** @@ -243,16 +223,14 @@ export class RobotVacuumCleaner { async refreshStatus(): Promise { if (!this.device.enableCloudService && this.OpenAPI) { this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} refreshStatus enableCloudService: ${this.device.enableCloudService}`); - /*} else if (this.BLE) { - await this.BLERefreshStatus();*/ + } else if (this.BLE) { + await this.BLERefreshStatus(); } else if (this.OpenAPI && this.platform.config.credentials?.token) { await this.openAPIRefreshStatus(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + - ` ${this.device.connectionType}, refreshStatus will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); } } @@ -268,96 +246,44 @@ export class RobotVacuumCleaner { this.getCustomBLEAddress(switchbot); // Start to monitor advertisement packets (async () => { - await switchbot.startScan({ - model: '?', - id: this.device.bleMac, - }); + // Start to monitor advertisement packets + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); // Set an event handler switchbot.onadvertisement = (ad: any) => { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.serviceData.model}`); - if (this.device.bleMac === ad.address && ad.serviceData.model === '?') { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.BLE_On = ad.serviceData.state; } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); } }; - // Wait 1 seconds + // Wait 10 seconds await switchbot.wait(this.scanDuration * 1000); // Stop to monitor await switchbot.stopScan(); // Update HomeKit - await this.BLEparseStatus(); + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); await this.updateHomeKitCharacteristics(); })(); - /*if (switchbot !== false) { - switchbot - .startScan({ - model: this.BLEmodel(), - id: this.device.bleMac, - }) - .then(async () => { - // Set an event handler - switchbot.onadvertisement = async (ad: ad) => { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} Config BLE Address: ${this.device.bleMac},` + - ` BLE Address Found: ${ad.address}`, - ); - this.BLE_On = ad.serviceData.state; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} state: ${ad.serviceData.state}, ` + - `delay: ${ad.serviceData.delay}, timer: ${ad.serviceData.timer}, syncUtcTime: ${ad.serviceData.syncUtcTime} ` + - `wifiRssi: ${ad.serviceData.wifiRssi}, overload: ${ad.serviceData.overload}, currentPower: ${ad.serviceData.currentPower}`, - ); - - if (ad.serviceData) { - this.BLE_IsConnected = true; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - await this.stopScanning(switchbot); - } else { - this.BLE_IsConnected = false; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} connected: ${this.BLE_IsConnected}`); - } - }; - // Wait - return await sleep(this.scanDuration * 1000); - }) - .then(async () => { - // Stop to monitor - await this.stopScanning(switchbot); - }) - .catch(async (e: any) => { - this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLERefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); - await this.BLERefreshConnection(switchbot); - }); - } else { + if (switchbot === undefined) { await this.BLERefreshConnection(switchbot); - }*/ + } } - async openAPIRefreshStatus() { + async openAPIRefreshStatus(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); try { - const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/status`, { - headers: this.platform.generateHeaders(), - }); + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); const deviceStatus: any = await body.json(); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.OpenAPI_On = deviceStatus.body.power; - this.OpenAPI_BatteryLevel = deviceStatus.body.battery; - this.OpenAPI_FirmwareRevision = deviceStatus.body.version; - this.openAPIparseStatus(); + this.openAPIparseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -365,10 +291,33 @@ export class RobotVacuumCleaner { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { onlineStatus, battery, workingStatus } = context; + const { On } = this.LightBulb; + const { BatteryLevel, ChargingState } = this.Battery; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (onlineStatus, battery, workingStatus) = ` + + `Webhook: (${onlineStatus}, ${battery}, ${workingStatus}), current: (${On}, ${BatteryLevel}, ${ChargingState})`); + this.LightBulb.On = onlineStatus === 'online' ? true : false; + this.Battery.ChargingState = workingStatus === 'Charging' + ? this.hap.Characteristic.ChargingState.CHARGING : this.hap.Characteristic.ChargingState.NOT_CHARGING; + this.Battery.BatteryLevel = battery; + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); } } @@ -390,9 +339,8 @@ export class RobotVacuumCleaner { await this.openAPIpushChanges(); } else { await this.offlineOff(); - this.debugWarnLog( - `${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + ` ${this.device.connectionType}, pushChanges will not happen.`, - ); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, pushChanges will not happen.`); } // Refresh the status from the API interval(15000) @@ -405,8 +353,9 @@ export class RobotVacuumCleaner { async BLEpushChanges(): Promise { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges`); - if (this.On !== this.accessory.context.On) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges On: ${this.On} OnCached: ${this.accessory.context.On}`); + if (this.LightBulb.On !== this.accessory.context.On) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEpushChanges` + + ` On: ${this.LightBulb.On} OnCached: ${this.accessory.context.On}`); const switchbot = await this.platform.connectBLE(); // Convert to BLE Address this.device.bleMac = this.device @@ -420,11 +369,11 @@ export class RobotVacuumCleaner { id: this.device.bleMac, }) .then(async (device_list: any) => { - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); - return await this.retry({ - max: this.maxRetry(), + this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); + return await this.retryBLE({ + max: await this.maxRetryBLE(), fn: async () => { - if (this.On) { + if (this.LightBulb.On) { return await device_list[0].turnOn({ id: this.device.bleMac }); } else { return await device_list[0].turnOff({ id: this.device.bleMac }); @@ -434,37 +383,26 @@ export class RobotVacuumCleaner { }) .then(() => { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Done.`); - this.On = false; + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `On: ${this.LightBulb.On} sent over BLE, sent successfully`); + this.LightBulb.On = false; }) .catch(async (e: any) => { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed BLEpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); await this.BLEPushConnection(); }); } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges.` + `On: ${this.On}, ` + `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No BLEpushChanges,` + + ` On: ${this.LightBulb.On}, OnCached: ${this.accessory.context.On}`); } } async openAPIpushChanges() { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushChanges`); - if (this.On !== this.accessory.context.On) { - let command = ''; - if (this.On) { - command = 'turnOn'; - } else { - command = 'turnOff'; - } - const bodyChange = JSON.stringify({ - command: `${command}`, - parameter: 'default', - commandType: 'command', - }); + if (this.LightBulb.On !== this.accessory.context.On) { + const bodyChange = await this.commands(); this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`); try { const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/commands`, { @@ -480,32 +418,30 @@ export class RobotVacuumCleaner { if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.debugLog( - `${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges.` + - `On: ${this.On}, ` + - `OnCached: ${this.accessory.context.On}`, - ); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No openAPIpushChanges, On: ${this.LightBulb.On}, ` + + `OnCached: ${this.accessory.context.On}`); } } async openAPIpushBrightnessChanges() { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIpushBrightnessChanges`); - const body = await this.brightnessCommands(); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${body},`); + const bodyChange = await this.brightnessCommands(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`); try { const { body, statusCode } = await request(`${Devices}/${this.device.deviceId}/commands`, { + body: bodyChange, method: 'POST', headers: this.platform.generateHeaders(), }); @@ -514,25 +450,25 @@ export class RobotVacuumCleaner { this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); } else { this.statusCode(statusCode); this.statusCode(deviceStatus.statusCode); } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIpushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } - private commands() { + async commands() { let command: string; let parameter: string; - if (this.On) { + if (this.LightBulb.On) { command = 'start'; parameter = 'default'; } else { @@ -547,44 +483,44 @@ export class RobotVacuumCleaner { return body; } - private brightnessCommands() { + async brightnessCommands(): Promise { let command: string; let parameter: string; - if (this.Brightness === 25) { + if (this.LightBulb.Brightness === 25) { command = 'PowLevel'; parameter = '0'; - } else if (this.Brightness === 50) { + } else if (this.LightBulb.Brightness === 50) { command = 'PowLevel'; parameter = '1'; - } else if (this.Brightness === 75) { + } else if (this.LightBulb.Brightness === 75) { command = 'PowLevel'; parameter = '2'; - } else if (this.Brightness === 100) { + } else if (this.LightBulb.Brightness === 100) { command = 'PowLevel'; parameter = '3'; } else { command = 'dock'; parameter = 'default'; } - const body = JSON.stringify({ + const bodyChange = JSON.stringify({ command: `${command}`, parameter: `${parameter}`, commandType: 'command', }); - return body; + return bodyChange; } /** * Handle requests to set the value of the "On" characteristic */ async OnSet(value: CharacteristicValue): Promise { - if (this.On === this.accessory.context.On) { + if (this.LightBulb.On === this.accessory.context.On) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set On: ${value}`); } else { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set On: ${value}`); } - this.On = value; + this.LightBulb.On = value; this.doRobotVacuumCleanerUpdate.next(); } @@ -592,78 +528,60 @@ export class RobotVacuumCleaner { * Handle requests to set the value of the "Brightness" characteristic */ async BrightnessSet(value: CharacteristicValue): Promise { - if (this.Brightness === this.accessory.context.Brightness) { + if (this.LightBulb.Brightness === this.accessory.context.Brightness) { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} No Changes, Set Brightness: ${value}`); - } else if (this.On) { + } else if (this.LightBulb.On) { this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`); } else { this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Set Brightness: ${value}`); } - this.Brightness = value; + this.LightBulb.Brightness = value; this.doRobotVacuumCleanerUpdate.next(); } async updateHomeKitCharacteristics(): Promise { // On - if (this.On === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.LightBulb.On === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} On: ${this.LightBulb.On}`); } else { - this.accessory.context.On = this.On; - this.robotVacuumCleanerService.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + this.accessory.context.On = this.LightBulb.On; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.On, this.LightBulb.On); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic On: ${this.LightBulb.On}`); } // Brightness - if (this.Brightness === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.Brightness}`); + if (this.LightBulb.Brightness === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Brightness: ${this.LightBulb.Brightness}`); } else { - this.accessory.context.Brightness = this.Brightness; - this.robotVacuumCleanerService.updateCharacteristic(this.hap.Characteristic.Brightness, this.Brightness); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Brightness: ${this.Brightness}`); + this.accessory.context.Brightness = this.LightBulb.Brightness; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.Brightness, this.LightBulb.Brightness); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic Brightness: ${this.LightBulb.Brightness}`); } // BatteryLevel - if (this.BatteryLevel === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}`); + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); } else { - this.accessory.context.BatteryLevel = this.BatteryLevel; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.BatteryLevel); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); } // StatusLowBattery - if (this.StatusLowBattery === undefined) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); } else { - this.accessory.context.StatusLowBattery = this.StatusLowBattery; - this.batteryService?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); - } - } - - async stopScanning(switchbot: any) { - switchbot.stopScan(); - if (this.BLE_IsConnected) { - await this.BLEparseStatus(); - await this.updateHomeKitCharacteristics(); + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); + } + // ChargingState + if (this.Battery.ChargingState === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ChargingState: ${this.Battery.ChargingState}`); } else { - await this.BLERefreshConnection(switchbot); - } - } - - async getCustomBLEAddress(switchbot: any) { - if (this.device.customBLEaddress && this.deviceLogging.includes('debug')) { - (async () => { - // Start to monitor advertisement packets - await switchbot.startScan({ - model: '?', - }); - // Set an event handler - switchbot.onadvertisement = (ad: any) => { - this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} ad: ${JSON.stringify(ad, null, ' ')}`); - }; - await sleep(10000); - // Stop to monitor - switchbot.stopScan(); - })(); + this.accessory.context.ChargingState = this.Battery.ChargingState; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.ChargingState, this.Battery.ChargingState); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` ChargingState: ${this.Battery.ChargingState}`); } } @@ -683,266 +601,13 @@ export class RobotVacuumCleaner { } } - async retry({ max, fn }: { max: number; fn: { (): any; (): Promise } }): Promise { - return fn().catch(async (e: any) => { - if (max === 0) { - throw e; - } - this.infoLog(e); - this.infoLog(`${this.device.deviceType}: ${this.accessory.displayName} Retrying`); - await sleep(1000); - return this.retry({ max: max - 1, fn }); - }); - } - - maxRetry(): number { - if (this.device.maxRetry) { - return this.device.maxRetry; - } else { - return 5; - } - } - - model(device: device & devicesConfig): string { - let model: string; - if (device.deviceType === 'Robot Vacuum Cleaner S1 Plus') { - model = 'W3011010'; - } else if (device.deviceType === 'Robot Vacuum Cleaner S1') { - model = 'W3011000'; - } else { - model = 'N/A'; - } - return model; - } - - async scan(device: device & devicesConfig): Promise { - if (device.scanDuration) { - this.scanDuration = this.accessory.context.scanDuration = device.scanDuration; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config scanDuration: ${this.scanDuration}`); - } - } else { - this.scanDuration = this.accessory.context.scanDuration = 1; - if (this.BLE) { - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Default scanDuration: ${this.scanDuration}`); - } - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - this.offlineOff(); - break; - case 171: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - this.offlineOff(); - break; - case 190: - this.errorLog( - `${this.device.deviceType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` - + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.deviceType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async offlineOff(): Promise { if (this.device.offline) { - await this.deviceContext(); - await this.updateHomeKitCharacteristics(); + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.On, false); } } async apiError(e: any): Promise { - this.robotVacuumCleanerService.updateCharacteristic(this.hap.Characteristic.On, e); - } - - async deviceContext() { - if (this.On === undefined) { - this.On = false; - } else { - this.On = this.accessory.context.On; - } - if (this.Brightness === undefined) { - this.Brightness = 0; - } else { - this.Brightness = this.accessory.context.Brightness; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async refreshRate(device: device & devicesConfig): Promise { - if (device.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); - } else if (this.platform.config.options!.refreshRate) { - this.deviceRefreshRate = this.accessory.context.refreshRate = this.platform.config.options!.refreshRate; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); - } - } - - async deviceConfig(device: device & devicesConfig): Promise { - let config = {}; - if (device.plug) { - config = device.plug; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.refreshRate !== undefined) { - config['refreshRate'] = device.refreshRate; - } - if (device.scanDuration !== undefined) { - config['scanDuration'] = device.scanDuration; - } - if (device.offline !== undefined) { - config['offline'] = device.offline; - } - if (device.maxRetry !== undefined) { - config['maxRetry'] = device.maxRetry; - } - if (device.webhook !== undefined) { - config['webhook'] = device.webhook; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: device & devicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.LightBulb.Service.updateCharacteristic(this.hap.Characteristic.On, e); } } diff --git a/src/device/waterdetector.ts b/src/device/waterdetector.ts new file mode 100644 index 00000000..63220e17 --- /dev/null +++ b/src/device/waterdetector.ts @@ -0,0 +1,358 @@ +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * waterdetector.ts: @switchbot/homebridge-switchbot. + */ +import { deviceBase } from './device.js'; +import { interval, Subject } from 'rxjs'; +import { Devices } from '../settings.js'; +import { skipWhile } from 'rxjs/operators'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; +import type { device, devicesConfig, serviceData, deviceStatus } from '../settings.js'; + +/** + * Platform Accessory + * An instance of this class is created for each accessory your platform registers + * Each accessory may expose multiple services of different service types. + */ +export class WaterDetector extends deviceBase { + // Services + private Battery: { + Name: CharacteristicValue + Service: Service; + BatteryLevel: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + ChargingState: CharacteristicValue; + }; + + private LeakSensor?: { + Name: CharacteristicValue; + Service: Service; + StatusActive: CharacteristicValue; + LeakDetected: CharacteristicValue; + }; + + // Updates + WaterDetectorUpdateInProgress!: boolean; + doWaterDetectorUpdate: Subject; + + constructor( + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, + ) { + super(platform, accessory, device); + + // this is subject we use to track when we need to POST changes to the SwitchBot API + this.doWaterDetectorUpdate = new Subject(); + this.WaterDetectorUpdateInProgress = false; + + // Initialize Battery Service + accessory.context.Battery = accessory.context.Battery ?? {}; + this.Battery = { + Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`, + Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel ?? 100, + StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + ChargingState: accessory.context.ChargingState ?? this.hap.Characteristic.ChargingState.NOT_CHARGEABLE, + }; + accessory.context.Battery = this.Battery as object; + + // Initialize Battery Characteristic + this.Battery.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) + .setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE) + .getCharacteristic(this.hap.Characteristic.BatteryLevel) + .onGet(() => { + return this.Battery.StatusLowBattery; + }); + + this.Battery.Service + .getCharacteristic(this.hap.Characteristic.StatusLowBattery) + .onGet(() => { + return this.Battery.StatusLowBattery; + }); + + // Initialize Leak Sensor Service + if (device.waterdetector?.hide_leak) { + if (this.LeakSensor) { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Leak Sensor Service`); + this.LeakSensor.Service = this.accessory.getService(this.hap.Service.LeakSensor) as Service; + accessory.removeService(this.LeakSensor.Service); + } else { + this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Leak Sensor Service Not Found`); + } + } else { + accessory.context.LeakSensor = accessory.context.LeakSensor ?? {}; + this.LeakSensor = { + Name: accessory.context.LeakSensor.Name ?? `${accessory.displayName} Leak Sensor`, + Service: accessory.getService(this.hap.Service.LeakSensor) ?? this.accessory.addService(this.hap.Service.LeakSensor) as Service, + StatusActive: accessory.context.StatusActive ?? false, + LeakDetected: accessory.context.LeakDetected ?? this.hap.Characteristic.LeakDetected.LEAK_NOT_DETECTED, + }; + accessory.context.LeakSensor = this.LeakSensor as object; + + // Initialize LeakSensor Characteristic + this.LeakSensor!.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LeakSensor.Name) + .setCharacteristic(this.hap.Characteristic.StatusActive, true) + .getCharacteristic(this.hap.Characteristic.LeakDetected) + .onGet(() => { + return this.LeakSensor!.LeakDetected; + }); + } + + // Retrieve initial values and updateHomekit + this.refreshStatus(); + + // Retrieve initial values and updateHomekit + this.updateHomeKitCharacteristics(); + + // Start an update interval + interval(this.deviceRefreshRate * 1000) + .pipe(skipWhile(() => this.WaterDetectorUpdateInProgress)) + .subscribe(async () => { + await this.refreshStatus(); + }); + + //regisiter webhook event handler + this.registerWebhook(accessory, device); + } + + async BLEparseStatus(serviceData: serviceData): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLEparseStatus`); + // Battery + this.Battery.BatteryLevel = Number(serviceData.battery); + if (this.Battery.BatteryLevel < 15) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + } else { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + } + this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}, StatusLowBattery: ${this.Battery.StatusLowBattery}`); + + // LeakDetected + if (this.device.waterdetector?.hide_leak) { + this.LeakSensor!.LeakDetected = serviceData.status!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LeakDetected: ${this.LeakSensor!.LeakDetected}`); + } + } + + async openAPIparseStatus(deviceStatus: deviceStatus): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIparseStatus`); + // StatusLowBattery + this.Battery.BatteryLevel = Number(deviceStatus.body.battery); + if (this.Battery.BatteryLevel < 10) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + } else { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + } + this.debugLog(`${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}, StatusLowBattery: ${this.Battery.StatusLowBattery}`); + + // BatteryLevel + if (Number.isNaN(this.Battery.BatteryLevel)) { + this.Battery.BatteryLevel = 100; + } + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); + + // LeakDetected + if (!this.device.waterdetector?.hide_leak) { + this.LeakSensor!.LeakDetected = deviceStatus.body.status!; + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LeakDetected: ${this.LeakSensor!.LeakDetected}`); + } + + // Firmware Version + const version = deviceStatus.body.version?.toString(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + if (deviceStatus.body.version) { + const deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + this.accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceVersion: ${this.accessory.context.deviceVersion}`); + } + } + + async refreshStatus(): Promise { + if (!this.device.enableCloudService && this.OpenAPI) { + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} refreshStatus enableCloudService: ${this.device.enableCloudService}`); + } else if (this.BLE) { + await this.BLERefreshStatus(); + } else if (this.OpenAPI && this.platform.config.credentials?.token) { + await this.openAPIRefreshStatus(); + } else { + await this.offlineOff(); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} Connection Type:` + + ` ${this.device.connectionType}, refreshStatus will not happen.`); + } + } + + async BLERefreshStatus(): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLERefreshStatus`); + const switchbot = await this.platform.connectBLE(); + // Convert to BLE Address + this.device.bleMac = this.device + .deviceId!.match(/.{1,2}/g)! + .join(':') + .toLowerCase(); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BLE Address: ${this.device.bleMac}`); + this.getCustomBLEAddress(switchbot); + // Start to monitor advertisement packets + (async () => { + // Start to monitor advertisement packets + await switchbot.startScan({ model: this.device.bleModel, id: this.device.bleMac }); + // Set an event handler + switchbot.onadvertisement = (ad: any) => { + if (this.device.bleMac === ad.address && ad.model === this.device.bleModel) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} ${JSON.stringify(ad, null, ' ')}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} address: ${ad.address}, model: ${ad.model}`); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); + } else { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} serviceData: ${JSON.stringify(ad.serviceData)}`); + } + }; + // Wait 10 seconds + await switchbot.wait(this.scanDuration * 1000); + // Stop to monitor + await switchbot.stopScan(); + // Update HomeKit + await this.BLEparseStatus(switchbot.onadvertisement.serviceData); + await this.updateHomeKitCharacteristics(); + })(); + if (switchbot === undefined) { + await this.BLERefreshConnection(switchbot); + } + } + + async openAPIRefreshStatus(): Promise { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} openAPIRefreshStatus`); + try { + const { body, statusCode } = await this.platform.retryRequest(this.deviceMaxRetries, this.deviceDelayBetweenRetries, + `${Devices}/${this.device.deviceId}/status`, { headers: this.platform.generateHeaders() }); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} statusCode: ${statusCode}`); + const deviceStatus: any = await body.json(); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); + this.debugWarnLog(`${this.device.deviceType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); + if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { + this.debugSuccessLog(`${this.device.deviceType}: ${this.accessory.displayName} ` + + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.openAPIparseStatus(deviceStatus); + this.updateHomeKitCharacteristics(); + } else { + this.statusCode(statusCode); + this.statusCode(deviceStatus.statusCode); + } + } catch (e: any) { + this.apiError(e); + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} failed openAPIRefreshStatus with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); + } + } + + async registerWebhook(accessory: PlatformAccessory, device: device & devicesConfig) { + if (device.webhook) { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is listening webhook.`); + this.platform.webhookEventHandler[device.deviceId] = async (context) => { + try { + this.debugLog(`${device.deviceType}: ${accessory.displayName} received Webhook: ${JSON.stringify(context)}`); + const { detectionState, battery } = context; + const { LeakDetected } = this.LeakSensor ? this.LeakSensor : { LeakDetected: undefined }; + const { BatteryLevel } = this.Battery ? this.Battery : { BatteryLevel: undefined }; + this.debugLog(`${device.deviceType}: ${accessory.displayName} (detectionState, battery) = Webhook: (${detectionState}, ${battery}), ` + + `current: (${LeakDetected}, ${BatteryLevel})`); + if (!device.waterdetector?.hide_leak) { + this.LeakSensor!.LeakDetected = detectionState; + } + this.Battery.BatteryLevel = battery; + this.updateHomeKitCharacteristics(); + } catch (e: any) { + this.errorLog(`${device.deviceType}: ${accessory.displayName} failed to handle webhook. Received: ${JSON.stringify(context)} Error: ${e}`); + } + }; + } else { + this.debugLog(`${device.deviceType}: ${accessory.displayName} is not listening webhook.`); + } + } + + /** + * Updates the status for each of the HomeKit Characteristics + */ + async updateHomeKitCharacteristics(): Promise { + const mqttmessage: string[] = []; + const entry = { time: Math.round(new Date().valueOf() / 1000) }; + if (!this.device.waterdetector?.hide_leak) { + if (this.LeakSensor!.LeakDetected === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} LeakDetected: ${this.LeakSensor!.LeakDetected}`); + } else { + if (this.device.mqttURL) { + mqttmessage.push(`"LeakDetected": ${this.LeakSensor!.LeakDetected}`); + } + if (this.device.history) { + entry['leak'] = this.LeakSensor!.LeakDetected; + } + this.accessory.context.LeakDetected = this.LeakSensor!.LeakDetected; + this.LeakSensor!.Service.updateCharacteristic(this.hap.Characteristic.LeakDetected, this.LeakSensor!.LeakDetected); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic LeakDetected: ${this.LeakSensor!.LeakDetected}`); + } + } + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); + } else { + if (this.device.mqttURL) { + mqttmessage.push(`"battery": ${this.Battery.BatteryLevel}`); + } + this.accessory.context.BatteryLevel = this.Battery.BatteryLevel; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); + } + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); + } else { + if (this.device.mqttURL) { + mqttmessage.push(`"lowBattery": ${this.Battery.StatusLowBattery}`); + } + this.accessory.context.StatusLowBattery = this.Battery.StatusLowBattery; + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceType}: ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); + } + if (this.device.mqttURL) { + this.mqttPublish(`{${mqttmessage.join(',')}}`); + } + if (!this.device.waterdetector?.hide_leak) { + if (Number(this.LeakSensor!.LeakDetected) > 0) { + // reject unreliable data + if (this.device.history) { + this.historyService?.addEntry(entry); + } + } + } + } + + async BLERefreshConnection(switchbot: any): Promise { + this.errorLog(`${this.device.deviceType}: ${this.accessory.displayName} wasn't able to establish BLE Connection, node-switchbot:` + + ` ${JSON.stringify(switchbot)}`); + if (this.platform.config.credentials?.token && this.device.connectionType === 'BLE/OpenAPI') { + this.warnLog(`${this.device.deviceType}: ${this.accessory.displayName} Using OpenAPI Connection to Refresh Status`); + await this.openAPIRefreshStatus(); + } + } + + async offlineOff(): Promise { + if (this.device.offline && !this.device.waterdetector?.hide_leak) { + this.LeakSensor!.Service.updateCharacteristic(this.hap.Characteristic.LeakDetected, this.hap.Characteristic.LeakDetected.LEAK_NOT_DETECTED); + } + } + + async apiError(e: any): Promise { + if (!this.device.waterdetector?.hide_leak) { + this.LeakSensor!.Service.updateCharacteristic(this.hap.Characteristic.LeakDetected, e); + } + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); + } +} diff --git a/src/homebridge-ui/public/index.html b/src/homebridge-ui/public/index.html index 0c9f7d7a..118c5647 100644 --- a/src/homebridge-ui/public/index.html +++ b/src/homebridge-ui/public/index.html @@ -1,8 +1,7 @@

homebridge-switchbot logo + alt="homebridge-switchbot logo" style="width: 50%" />

@@ -162,7 +168,7 @@
Help/About
document.getElementById('displayName').innerHTML = thisAcc.displayName; document.getElementById('deviceID').innerHTML = context.deviceID; document.getElementById('model').innerHTML = context.model; - document.getElementById('firmwareRevision').innerHTML = context.firmwareRevision || 'N/A'; + document.getElementById('version').innerHTML = context.version; document.getElementById('deviceType').innerHTML = context.deviceType; document.getElementById('connectionType').innerHTML = context.connectionType; document.getElementById('deviceTable').style.display = 'inline-table'; @@ -237,4 +243,4 @@
Help/About
homebridge.hideSpinner(); } })(); - + \ No newline at end of file diff --git a/src/homebridge-ui/server.ts b/src/homebridge-ui/server.ts index 1cae72d4..65e33b82 100644 --- a/src/homebridge-ui/server.ts +++ b/src/homebridge-ui/server.ts @@ -31,7 +31,7 @@ class PluginUiServer extends HomebridgePluginUiServer { } // Return the array return devicesToReturn; - } catch (err) { + } catch { // Just return an empty accessory list in case of any errors return []; } diff --git a/src/index.ts b/src/index.ts index f3540885..f1989218 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,11 @@ -/* Copyright(C) 2021-2023, SwitchBot (https://github.com/SwitchBot). All rights reserved. +/* Copyright(C) 2021-2024, SwitchBot (https://github.com/SwitchBot). All rights reserved. * * index.ts: @switchbot/homebridge-switchbot plugin registration. */ -import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'; -import { API } from 'homebridge'; import { SwitchBotPlatform } from './platform.js'; +import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'; + +import type { API } from 'homebridge'; // Register our platform with homebridge. export default (api: API): void => { diff --git a/src/irdevice/airconditioner.ts b/src/irdevice/airconditioner.ts index e97e879e..21da7df8 100644 --- a/src/irdevice/airconditioner.ts +++ b/src/irdevice/airconditioner.ts @@ -1,30 +1,39 @@ -import { CharacteristicValue, PlatformAccessory, Service, API, Logging, HAP } from 'homebridge'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * airconditioners.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, irDevicesConfig, irdevice, SwitchBotPlatformConfig } from '../settings.js'; +import { Devices } from '../settings.js'; +import { irdeviceBase } from './irdevice.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { irDevicesConfig, irdevice } from '../settings.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class AirConditioner { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class AirConditioner extends irdeviceBase { // Services - coolerService!: Service; - - // Characteristic Values - Active!: CharacteristicValue; - RotationSpeed!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - CurrentTemperature!: CharacteristicValue; - ThresholdTemperature!: CharacteristicValue; - CurrentRelativeHumidity?: CharacteristicValue; - TargetHeaterCoolerState!: CharacteristicValue; - CurrentHeaterCoolerState!: CharacteristicValue; + private HeaterCooler: { + Name: CharacteristicValue; + Service: Service; + Active: CharacteristicValue; + CurrentHeaterCoolerState: CharacteristicValue; + TargetHeaterCoolerState: CharacteristicValue; + CurrentTemperature: CharacteristicValue; + ThresholdTemperature: CharacteristicValue; + RotationSpeed: CharacteristicValue; + }; + + meter?: PlatformAccessory; + private HumiditySensor?: { + Name: CharacteristicValue; + Service: Service; + CurrentRelativeHumidity: CharacteristicValue; + }; // Others state!: string; @@ -33,108 +42,96 @@ export class AirConditioner { CurrentMode!: number; ValidValues: number[]; CurrentFanSpeed!: number; - static MODE_AUTO: number; - static MODE_COOL: number; - static MODE_HEAT: number; // Config - deviceLogging!: string; hide_automode?: boolean; set_max_heat?: number; set_min_heat?: number; set_max_cool?: number; set_min_cool?: number; - disablePushOn?: boolean; - disablePushOff?: boolean; - meter?: PlatformAccessory; - disablePushDetail?: boolean; - - private readonly valid12 = [1, 2]; - private readonly valid012 = [0, 1, 2]; constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: irdevice & irDevicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: irdevice & irDevicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // default placeholders - this.deviceLogs(device); - this.deviceContext(); - this.disablePushOnChanges(device); - this.disablePushOffChanges(device); - this.disablePushDetailChanges(device); - this.deviceConfig(device); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, device.remoteType) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Television service if it exists, otherwise create a new Television service - // you can create multiple services for each accessory - const coolerService = `${accessory.displayName} ${device.remoteType}`; - (this.coolerService = accessory.getService(this.hap.Service.HeaterCooler) - || accessory.addService(this.hap.Service.HeaterCooler)), coolerService; - - this.coolerService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.coolerService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.coolerService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } + super(platform, accessory, device); - // handle on / off events using the Active characteristic - this.coolerService.getCharacteristic(this.hap.Characteristic.Active).onSet(this.ActiveSet.bind(this)); - - this.coolerService.getCharacteristic(this.hap.Characteristic.CurrentTemperature).onGet(this.CurrentTemperatureGet.bind(this)); + // default placeholders + this.getAirConditionerConfigSettings(accessory, device); this.ValidValues = this.hide_automode ? [1, 2] : [0, 1, 2]; - if (this.device.irair?.meterType && this.device.irair?.meterId) { - const meterUuid = this.platform.api.hap.uuid.generate(`${this.device.irair.meterId}-${this.device.irair.meterType}`); - this.meter = this.platform.accessories.find((accessory) => accessory.UUID === meterUuid); - } - if (this.meter) { - this.coolerService.getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity).onGet(this.CurrentRelativeHumidityGet.bind(this)); - } + // Initialize HeaterCooler Service + accessory.context.HeaterCooler = accessory.context.HeaterCooler ?? {}; + this.HeaterCooler = { + Name: accessory.context.HeaterCooler.Name ?? `${accessory.displayName} ${device.remoteType}`, + Service: accessory.getService(this.hap.Service.HeaterCooler) ?? accessory.addService(this.hap.Service.HeaterCooler) as Service, + Active: accessory.context.Active ?? this.hap.Characteristic.Active.INACTIVE, + CurrentHeaterCoolerState: accessory.context.CurrentHeaterCoolerState ?? this.hap.Characteristic.CurrentHeaterCoolerState.IDLE, + TargetHeaterCoolerState: accessory.context.TargetHeaterCoolerState ?? this.hap.Characteristic.TargetHeaterCoolerState.AUTO, + CurrentTemperature: accessory.context.CurrentTemperature ?? 24, + ThresholdTemperature: accessory.context.ThresholdTemperature ?? 24, + RotationSpeed: accessory.context.RotationSpeed ?? 4, + }; + accessory.context.HeaterCooler = this.HeaterCooler as object; + + this.HeaterCooler.Service + .setCharacteristic(this.hap.Characteristic.Name, this.HeaterCooler.Name) + .getCharacteristic(this.hap.Characteristic.Active) + .onGet(() => { + return this.HeaterCooler.Active; + }) + .onSet(this.ActiveSet.bind(this)); + + this.HeaterCooler.Service + .getCharacteristic(this.hap.Characteristic.CurrentTemperature) + .onGet(async () => { + return await this.CurrentTemperatureGet(); + }); - this.coolerService + this.HeaterCooler.Service .getCharacteristic(this.hap.Characteristic.TargetHeaterCoolerState) .setProps({ validValues: this.ValidValues, }) - .onGet(this.TargetHeaterCoolerStateGet.bind(this)) + .onGet(async () => { + return await this.TargetHeaterCoolerStateGet(); + }) .onSet(this.TargetHeaterCoolerStateSet.bind(this)); - this.coolerService.getCharacteristic(this.hap.Characteristic.CurrentHeaterCoolerState).onGet(this.CurrentHeaterCoolerStateGet.bind(this)); + this.HeaterCooler.Service + .getCharacteristic(this.hap.Characteristic.CurrentHeaterCoolerState) + .onGet(async () => { + return await this.CurrentHeaterCoolerStateGet(); + }); - this.coolerService + this.HeaterCooler.Service .getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature) .setProps({ minValue: this.set_min_heat, maxValue: this.set_max_heat, minStep: 0.5, }) - .onGet(this.ThresholdTemperatureGet.bind(this)) + .onGet(async () => { + return await this.ThresholdTemperatureGet(); + }) .onSet(this.ThresholdTemperatureSet.bind(this)); - this.coolerService + this.HeaterCooler.Service .getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature) .setProps({ minValue: this.set_min_cool, maxValue: this.set_max_cool, minStep: 0.5, }) - .onGet(this.ThresholdTemperatureGet.bind(this)) + .onGet(async () => { + return await this.ThresholdTemperatureGet(); + }) .onSet(this.ThresholdTemperatureSet.bind(this)); - this.coolerService + this.HeaterCooler.Service .getCharacteristic(this.hap.Characteristic.RotationSpeed) .setProps({ format: 'int', @@ -142,8 +139,37 @@ export class AirConditioner { minValue: 1, maxValue: 4, }) - .onGet(this.RotationSpeedGet.bind(this)) + .onGet(async () => { + return await this.RotationSpeedGet(); + }) .onSet(this.RotationSpeedSet.bind(this)); + + // Initialize HumiditySensor property + + if (this.device.irair?.meterType && this.device.irair?.meterId) { + const meterUuid = this.platform.api.hap.uuid.generate(`${this.device.irair.meterId}-${this.device.irair.meterType}`); + this.meter = this.platform.accessories.find((accessory) => accessory.UUID === meterUuid); + accessory.context.HumiditySensor = accessory.context.HumiditySensor ?? {}; + this.HumiditySensor = { + Name: accessory.context.HumiditySensor ?? this.meter!.displayName, + Service: this.meter!.getService(this.hap.Service.HumiditySensor) ?? this.meter!.addService(this.hap.Service.HumiditySensor) as Service, + CurrentRelativeHumidity: this.meter!.context.CurrentRelativeHumidity || 0, + }; + accessory.context.HumiditySensor = this.HumiditySensor as object; + } + + if (this.device.irair?.meterType && this.device.irair?.meterId) { + const meterUuid = this.platform.api.hap.uuid.generate(`${this.device.irair.meterId}-${this.device.irair.meterType}`); + this.meter = this.platform.accessories.find((accessory) => accessory.UUID === meterUuid); + } + + if (this.meter) { + this.HumiditySensor!.Service + .getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity) + .onGet(async () => { + return await this.CurrentRelativeHumidityGet(); + }); + } } /** @@ -156,11 +182,9 @@ export class AirConditioner { * AirConditioner: "command" "highSpeed" "default" = fan speed to high */ async pushAirConditionerOnChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerOnChanges Active: ${this.Active},` + - ` disablePushOn: ${this.disablePushOn}`, - ); - if (this.Active === this.hap.Characteristic.Active.ACTIVE && !this.disablePushOn) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerOnChanges Active: ${this.HeaterCooler.Active},` + + ` disablePushOn: ${this.disablePushOn}`); + if (this.HeaterCooler.Active === this.hap.Characteristic.Active.ACTIVE && !this.disablePushOn) { const commandType: string = await this.commandType(); const command: string = await this.commandOn(); const bodyChange = JSON.stringify({ @@ -173,11 +197,9 @@ export class AirConditioner { } async pushAirConditionerOffChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerOffChanges Active: ${this.Active},` + - ` disablePushOff: ${this.disablePushOff}`, - ); - if (this.Active === this.hap.Characteristic.Active.INACTIVE && !this.disablePushOff) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerOffChanges Active: ${this.HeaterCooler.Active},` + + ` disablePushOff: ${this.disablePushOff}`); + if (this.HeaterCooler.Active === this.hap.Characteristic.Active.INACTIVE && !this.disablePushOff) { const commandType: string = await this.commandType(); const command: string = await this.commandOff(); const bodyChange = JSON.stringify({ @@ -190,13 +212,11 @@ export class AirConditioner { } async pushAirConditionerStatusChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerStatusChanges Active: ${this.Active},` + - ` disablePushOff: ${this.disablePushOff}, disablePushOn: ${this.disablePushOn}`, - ); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerStatusChanges Active: ${this.HeaterCooler.Active},` + + ` disablePushOff: ${this.disablePushOff}, disablePushOn: ${this.disablePushOn}`); if (!this.Busy) { this.Busy = true; - this.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.IDLE; + this.HeaterCooler.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.IDLE; } clearTimeout(this.Timeout); @@ -205,10 +225,8 @@ export class AirConditioner { } async pushAirConditionerDetailsChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerDetailsChanges Active: ${this.Active},` + - ` disablePushOff: ${this.disablePushOff}, disablePushOn: ${this.disablePushOn}`, - ); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerDetailsChanges Active: ${this.HeaterCooler.Active},` + + ` disablePushOff: ${this.disablePushOff}, disablePushOn: ${this.disablePushOn}`); //await this.deviceContext(); if (this.CurrentMode === undefined) { this.CurrentMode = 1; @@ -216,20 +234,18 @@ export class AirConditioner { if (this.CurrentFanSpeed === undefined) { this.CurrentFanSpeed = 1; } - if (this.Active === this.hap.Characteristic.Active.ACTIVE) { + if (this.HeaterCooler.Active === this.hap.Characteristic.Active.ACTIVE) { this.state = 'on'; } else { this.state = 'off'; } if (this.CurrentMode === 1) { // Remove or make configurable? - this.ThresholdTemperature = 25; - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} CurrentMode: ${this.CurrentMode},` + - ` ThresholdTemperature: ${this.ThresholdTemperature}`, - ); + this.HeaterCooler.ThresholdTemperature = 25; + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} CurrentMode: ${this.CurrentMode},` + + ` ThresholdTemperature: ${this.HeaterCooler.ThresholdTemperature}`); } - const parameter = `${this.ThresholdTemperature},${this.CurrentMode},${this.CurrentFanSpeed},${this.state}`; + const parameter = `${this.HeaterCooler.ThresholdTemperature},${this.CurrentMode},${this.CurrentFanSpeed},${this.state}`; await this.UpdateCurrentHeaterCoolerState(); const bodyChange = JSON.stringify({ @@ -242,19 +258,91 @@ export class AirConditioner { } private async UpdateCurrentHeaterCoolerState() { - if (this.Active === this.hap.Characteristic.Active.ACTIVE) { - await this.deviceContext(); - if (this.ThresholdTemperature < this.CurrentTemperature && - this.TargetHeaterCoolerState !== this.hap.Characteristic.TargetHeaterCoolerState.HEAT) { - this.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.COOLING; - } else if (this.ThresholdTemperature > this.CurrentTemperature && - this.TargetHeaterCoolerState !== this.hap.Characteristic.TargetHeaterCoolerState.COOL) { - this.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.HEATING; + if (this.HeaterCooler.Active === this.hap.Characteristic.Active.ACTIVE) { + + if (this.HeaterCooler.Active === undefined) { + this.HeaterCooler.Active = this.hap.Characteristic.Active.INACTIVE; + } else if (this.HeaterCooler.Active) { + this.HeaterCooler.Active; + } else { + this.HeaterCooler.Active = this.accessory.context.Active; + } + + if (this.HeaterCooler.CurrentTemperature === undefined && this.accessory.context.CurrentTemperature === undefined) { + this.HeaterCooler.CurrentTemperature = 24; + } else { + this.HeaterCooler.CurrentTemperature = this.HeaterCooler.CurrentTemperature || this.accessory.context.CurrentTemperature; + } + + if (this.HeaterCooler.ThresholdTemperature === undefined && this.accessory.context.ThresholdTemperature === undefined) { + this.HeaterCooler.ThresholdTemperature = 24; + } else { + this.HeaterCooler.ThresholdTemperature = this.HeaterCooler.ThresholdTemperature || this.accessory.context.ThresholdTemperature; + } + + if (this.HeaterCooler.RotationSpeed === undefined && this.accessory.context.RotationSpeed === undefined) { + this.HeaterCooler.RotationSpeed = 4; + } else { + this.HeaterCooler.RotationSpeed = this.HeaterCooler.RotationSpeed || this.accessory.context.RotationSpeed; + } + + if (this.device.irair?.hide_automode) { + this.hide_automode = this.device.irair?.hide_automode; + this.accessory.context.hide_automode = this.hide_automode; + } else { + this.hide_automode = this.device.irair?.hide_automode; + this.accessory.context.hide_automode = this.hide_automode; + } + + if (this.device.irair?.set_max_heat) { + this.set_max_heat = this.device.irair?.set_max_heat; + this.accessory.context.set_max_heat = this.set_max_heat; + } else { + this.set_max_heat = 35; + this.accessory.context.set_max_heat = this.set_max_heat; + } + if (this.device.irair?.set_min_heat) { + this.set_min_heat = this.device.irair?.set_min_heat; + this.accessory.context.set_min_heat = this.set_min_heat; + } else { + this.set_min_heat = 0; + this.accessory.context.set_min_heat = this.set_min_heat; + } + + if (this.device.irair?.set_max_cool) { + this.set_max_cool = this.device.irair?.set_max_cool; + this.accessory.context.set_max_cool = this.set_max_cool; + } else { + this.set_max_cool = 35; + this.accessory.context.set_max_cool = this.set_max_cool; + } + if (this.device.irair?.set_min_cool) { + this.set_min_cool = this.device.irair?.set_min_cool; + this.accessory.context.set_min_cool = this.set_min_cool; + } else { + this.set_min_cool = 0; + this.accessory.context.set_min_cool = this.set_min_cool; + } + + if (this.meter) { + if (this.HumiditySensor!.CurrentRelativeHumidity === undefined && this.accessory.context.CurrentRelativeHumidity === undefined) { + this.HumiditySensor!.CurrentRelativeHumidity = 0; + } else { + this.HumiditySensor!.CurrentRelativeHumidity = this.HumiditySensor!.CurrentRelativeHumidity + || this.accessory.context.CurrentRelativeHumidity; + } + } + if (this.HeaterCooler.ThresholdTemperature < this.HeaterCooler.CurrentTemperature && + this.HeaterCooler.TargetHeaterCoolerState !== this.hap.Characteristic.TargetHeaterCoolerState.HEAT) { + this.HeaterCooler.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.COOLING; + } else if (this.HeaterCooler.ThresholdTemperature > this.HeaterCooler.CurrentTemperature && + this.HeaterCooler.TargetHeaterCoolerState !== this.hap.Characteristic.TargetHeaterCoolerState.COOL) { + this.HeaterCooler.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.HEATING; } else { - this.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.IDLE; + this.HeaterCooler.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.IDLE; } } else { - this.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.INACTIVE; + this.HeaterCooler.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.INACTIVE; } } @@ -273,8 +361,10 @@ export class AirConditioner { this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -282,20 +372,14 @@ export class AirConditioner { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.warnLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`, - ); - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` Connection Type: ${this.device.connectionType}, disablePushDetails: ${this.disablePushDetail}`, - ); + this.warnLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` Connection Type: ${this.device.connectionType}, disablePushDetails: ${this.disablePushDetail}`); this.updateHomeKitCharacteristics(); } } @@ -303,39 +387,36 @@ export class AirConditioner { async CurrentTemperatureGet(): Promise { if (this.meter?.context?.CurrentTemperature) { this.accessory.context.CurrentTemperature = this.meter.context.CurrentTemperature; - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} ` - + `Using CurrentTemperature from ${this.meter.context.deviceType} (${this.meter.context.deviceID})`, - ); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + + `Using CurrentTemperature from ${this.meter.context.deviceType} (${this.meter.context.deviceID})`); } - this.CurrentTemperature = this.accessory.context.CurrentTemperature || 24; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Get CurrentTemperature: ${this.CurrentTemperature}`); - return this.CurrentTemperature; + this.HeaterCooler.CurrentTemperature = this.accessory.context.CurrentTemperature || 24; + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Get CurrentTemperature: ${this.HeaterCooler.CurrentTemperature}`); + return this.HeaterCooler.CurrentTemperature; } async CurrentRelativeHumidityGet(): Promise { if (this.meter?.context?.CurrentRelativeHumidity) { this.accessory.context.CurrentRelativeHumidity = this.meter.context.CurrentRelativeHumidity; - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} ` - + `Using CurrentRelativeHumidity from ${this.meter.context.deviceType} (${this.meter.context.deviceID})`, - ); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + + `Using CurrentRelativeHumidity from ${this.meter.context.deviceType} (${this.meter.context.deviceID})`); } - this.CurrentRelativeHumidity = this.accessory.context.CurrentRelativeHumidity || 0; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Get CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); - return this.CurrentRelativeHumidity as CharacteristicValue; + this.HumiditySensor!.CurrentRelativeHumidity = this.accessory.context.CurrentRelativeHumidity || 0; + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Get` + + ` CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}`); + return this.HumiditySensor!.CurrentRelativeHumidity as CharacteristicValue; } async RotationSpeedGet(): Promise { if (!this.CurrentFanSpeed || this.CurrentFanSpeed === 1) { - this.RotationSpeed = 4; + this.HeaterCooler.RotationSpeed = 4; } else { - this.RotationSpeed = this.CurrentFanSpeed - 1; + this.HeaterCooler.RotationSpeed = this.CurrentFanSpeed - 1; } - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Get RotationSpeed: ${this.RotationSpeed}`); - return this.RotationSpeed; + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Get RotationSpeed: ${this.HeaterCooler.RotationSpeed}`); + return this.HeaterCooler.RotationSpeed; } async RotationSpeedSet(value: CharacteristicValue): Promise { @@ -344,37 +425,35 @@ export class AirConditioner { } else { this.CurrentFanSpeed = Number(value) + 1; } - this.RotationSpeed = value; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName}` + - `Set RotationSpeed: ${this.RotationSpeed}, CurrentFanSpeed: ${this.CurrentFanSpeed}`); + this.HeaterCooler.RotationSpeed = value; + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + `Set RotationSpeed: ${this.HeaterCooler.RotationSpeed}, CurrentFanSpeed: ${this.CurrentFanSpeed}`); this.pushAirConditionerStatusChanges(); } async ActiveSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Set Active: ${value}`); - this.Active = value; - if (this.Active === this.hap.Characteristic.Active.ACTIVE) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerOnChanges, Active: ${this.Active}`); + this.HeaterCooler.Active = value; + if (this.HeaterCooler.Active === this.hap.Characteristic.Active.ACTIVE) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerOnChanges, Active: ${this.HeaterCooler.Active}`); if (this.disablePushOn) { this.pushAirConditionerStatusChanges(); } else { this.pushAirConditionerOnChanges(); } } else { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerOffChanges, Active: ${this.Active}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirConditionerOffChanges, Active: ${this.HeaterCooler.Active}`); this.pushAirConditionerOffChanges(); } } async TargetHeaterCoolerStateGet(): Promise { - const targetState = this.TargetHeaterCoolerState || this.accessory.context.TargetHeaterCoolerState; - this.TargetHeaterCoolerState = this.ValidValues.includes(targetState) ? targetState : this.ValidValues[0]; - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} Get (${this.getTargetHeaterCoolerStateName()})` + - ` TargetHeaterCoolerState: ${this.TargetHeaterCoolerState}, ValidValues: ${this.ValidValues}, hide_automode: ${this.hide_automode}`, - ); - return this.TargetHeaterCoolerState; + const targetState = this.HeaterCooler.TargetHeaterCoolerState || this.accessory.context.TargetHeaterCoolerState; + this.HeaterCooler.TargetHeaterCoolerState = this.ValidValues.includes(targetState) ? targetState : this.ValidValues[0]; + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Get (${this.getTargetHeaterCoolerStateName()}) TargetHeaterCoolerState:` + + ` ${this.HeaterCooler.TargetHeaterCoolerState}, ValidValues: ${this.ValidValues}, hide_automode: ${this.hide_automode}`); + return this.HeaterCooler.TargetHeaterCoolerState; } async TargetHeaterCoolerStateSet(value: CharacteristicValue): Promise { @@ -385,48 +464,47 @@ export class AirConditioner { } else if (value === this.hap.Characteristic.TargetHeaterCoolerState.COOL) { this.TargetHeaterCoolerStateCOOL(); } else { - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Set TargetHeaterCoolerState: ${this.TargetHeaterCoolerState},` + - ` hide_automode: ${this.hide_automode} `, - ); + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Set TargetHeaterCoolerState: ` + + `${this.HeaterCooler.TargetHeaterCoolerState}, hide_automode: ${this.hide_automode} `); } this.pushAirConditionerStatusChanges(); } async TargetHeaterCoolerStateAUTO(): Promise { - this.TargetHeaterCoolerState = this.hap.Characteristic.TargetHeaterCoolerState.AUTO; + this.HeaterCooler.TargetHeaterCoolerState = this.hap.Characteristic.TargetHeaterCoolerState.AUTO; this.CurrentMode = 1; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Set (AUTO) TargetHeaterCoolerState: ${this.TargetHeaterCoolerState}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Set (AUTO)` + + ` TargetHeaterCoolerState: ${this.HeaterCooler.TargetHeaterCoolerState}`); this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Switchbot CurrentMode: ${this.CurrentMode}`); } async TargetHeaterCoolerStateCOOL(): Promise { - this.TargetHeaterCoolerState = this.hap.Characteristic.TargetHeaterCoolerState.COOL; + this.HeaterCooler.TargetHeaterCoolerState = this.hap.Characteristic.TargetHeaterCoolerState.COOL; this.CurrentMode = 2; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Set (COOL) TargetHeaterCoolerState: ${this.TargetHeaterCoolerState}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Set (COOL)` + + ` TargetHeaterCoolerState: ${this.HeaterCooler.TargetHeaterCoolerState}`); this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Switchbot CurrentMode: ${this.CurrentMode}`); } async TargetHeaterCoolerStateHEAT(): Promise { - this.TargetHeaterCoolerState = this.hap.Characteristic.TargetHeaterCoolerState.HEAT; + this.HeaterCooler.TargetHeaterCoolerState = this.hap.Characteristic.TargetHeaterCoolerState.HEAT; this.CurrentMode = 5; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Set (HEAT) TargetHeaterCoolerState: ${this.TargetHeaterCoolerState}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Set (HEAT)` + + ` TargetHeaterCoolerState: ${this.HeaterCooler.TargetHeaterCoolerState}`); this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Switchbot CurrentMode: ${this.CurrentMode}`); } async CurrentHeaterCoolerStateGet(): Promise { await this.UpdateCurrentHeaterCoolerState(); - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` Get (${this.getTargetHeaterCoolerStateName()}) CurrentHeaterCoolerState: ${this.CurrentHeaterCoolerState}`, - ); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` Get (${this.getTargetHeaterCoolerStateName()}) CurrentHeaterCoolerState: ${this.HeaterCooler.CurrentHeaterCoolerState}`); - return this.CurrentHeaterCoolerState; + return this.HeaterCooler.CurrentHeaterCoolerState; } private getTargetHeaterCoolerStateName(): string { - switch (this.TargetHeaterCoolerState) { + switch (this.HeaterCooler.TargetHeaterCoolerState) { case this.hap.Characteristic.TargetHeaterCoolerState.AUTO: return 'AUTO'; case this.hap.Characteristic.TargetHeaterCoolerState.HEAT: @@ -434,412 +512,177 @@ export class AirConditioner { case this.hap.Characteristic.TargetHeaterCoolerState.COOL: return 'COOL'; default: - return this.TargetHeaterCoolerState.toString(); + return this.HeaterCooler.TargetHeaterCoolerState.toString(); } } async ThresholdTemperatureGet(): Promise { - await this.deviceContext(); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Get ThresholdTemperature: ${this.ThresholdTemperature}`); - return this.ThresholdTemperature; + + if (this.HeaterCooler.Active === undefined) { + this.HeaterCooler.Active = this.hap.Characteristic.Active.INACTIVE; + } else if (this.HeaterCooler.Active) { + this.HeaterCooler.Active; + } else { + this.HeaterCooler.Active = this.accessory.context.Active; + } + + if (this.HeaterCooler.CurrentTemperature === undefined && this.accessory.context.CurrentTemperature === undefined) { + this.HeaterCooler.CurrentTemperature = 24; + } else { + this.HeaterCooler.CurrentTemperature = this.HeaterCooler.CurrentTemperature || this.accessory.context.CurrentTemperature; + } + + if (this.HeaterCooler.ThresholdTemperature === undefined && this.accessory.context.ThresholdTemperature === undefined) { + this.HeaterCooler.ThresholdTemperature = 24; + } else { + this.HeaterCooler.ThresholdTemperature = this.HeaterCooler.ThresholdTemperature || this.accessory.context.ThresholdTemperature; + } + + if (this.HeaterCooler.RotationSpeed === undefined && this.accessory.context.RotationSpeed === undefined) { + this.HeaterCooler.RotationSpeed = 4; + } else { + this.HeaterCooler.RotationSpeed = this.HeaterCooler.RotationSpeed || this.accessory.context.RotationSpeed; + } + + await this.getAirConditionerConfigSettings(this.accessory, this.device); + + if (this.meter) { + if (this.HumiditySensor!.CurrentRelativeHumidity === undefined && this.accessory.context.CurrentRelativeHumidity === undefined) { + this.HumiditySensor!.CurrentRelativeHumidity = 0; + } else { + this.HumiditySensor!.CurrentRelativeHumidity = this.HumiditySensor!.CurrentRelativeHumidity || this.accessory.context.CurrentRelativeHumidity; + } + } + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Get ThresholdTemperature: ${this.HeaterCooler.ThresholdTemperature}`); + return this.HeaterCooler.ThresholdTemperature; } async ThresholdTemperatureSet(value: CharacteristicValue): Promise { - this.ThresholdTemperature = value; - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} Set ThresholdTemperature: ${this.ThresholdTemperature},` + - ` ThresholdTemperatureCached: ${this.accessory.context.ThresholdTemperature}`, - ); + this.HeaterCooler.ThresholdTemperature = value; + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Set ThresholdTemperature: ${this.HeaterCooler.ThresholdTemperature},` + + ` ThresholdTemperatureCached: ${this.accessory.context.ThresholdTemperature}`); this.pushAirConditionerStatusChanges(); } async updateHomeKitCharacteristics(): Promise { // Active - if (this.Active === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${this.Active}`); + if (this.HeaterCooler.Active === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${this.HeaterCooler.Active}`); } else { - this.accessory.context.Active = this.Active; - this.coolerService?.updateCharacteristic(this.hap.Characteristic.Active, this.Active); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Active}`); + this.accessory.context.Active = this.HeaterCooler.Active; + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.Active, this.HeaterCooler.Active); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.HeaterCooler.Active}`); } // RotationSpeed - if (this.RotationSpeed === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} RotationSpeed: ${this.RotationSpeed}`); + if (this.HeaterCooler.RotationSpeed === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} RotationSpeed: ${this.HeaterCooler.RotationSpeed}`); } else { - this.accessory.context.RotationSpeed = this.RotationSpeed; - this.coolerService?.updateCharacteristic(this.hap.Characteristic.RotationSpeed, this.RotationSpeed); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic RotationSpeed: ${this.RotationSpeed}`); + this.accessory.context.RotationSpeed = this.HeaterCooler.RotationSpeed; + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, this.HeaterCooler.RotationSpeed); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` RotationSpeed: ${this.HeaterCooler.RotationSpeed}`); } // CurrentTemperature - if (this.CurrentTemperature === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}`); + if (this.HeaterCooler.CurrentTemperature === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} CurrentTemperature: ${this.HeaterCooler.CurrentTemperature}`); } else { - this.accessory.context.CurrentTemperature = this.CurrentTemperature; - this.coolerService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.CurrentTemperature); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.CurrentTemperature}`); + this.accessory.context.CurrentTemperature = this.HeaterCooler.CurrentTemperature; + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.HeaterCooler.CurrentTemperature); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentTemperature: ${this.HeaterCooler.CurrentTemperature}`); } // CurrentRelativeHumidity if (this.meter) { - if (this.CurrentRelativeHumidity === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + if (this.HumiditySensor!.CurrentRelativeHumidity === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}`); } else { - this.accessory.context.CurrentRelativeHumidity = this.CurrentRelativeHumidity; - this.coolerService?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.CurrentRelativeHumidity); - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`, - ); + this.accessory.context.CurrentRelativeHumidity = this.HumiditySensor!.CurrentRelativeHumidity; + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, + this.HumiditySensor!.CurrentRelativeHumidity); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}`); } } // TargetHeaterCoolerState - if (this.TargetHeaterCoolerState === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} TargetHeaterCoolerState: ${this.TargetHeaterCoolerState}`); + if (this.HeaterCooler.TargetHeaterCoolerState === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} TargetHeaterCoolerState: ${this.HeaterCooler.TargetHeaterCoolerState}`); } else { - this.accessory.context.TargetHeaterCoolerState = this.TargetHeaterCoolerState; - this.coolerService?.updateCharacteristic(this.hap.Characteristic.TargetHeaterCoolerState, this.TargetHeaterCoolerState); - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + ` updateCharacteristic TargetHeaterCoolerState: ${this.TargetHeaterCoolerState}`, - ); + this.accessory.context.TargetHeaterCoolerState = this.HeaterCooler.TargetHeaterCoolerState; + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.TargetHeaterCoolerState, this.HeaterCooler.TargetHeaterCoolerState); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetHeaterCoolerState: ${this.HeaterCooler.TargetHeaterCoolerState}`); } // CurrentHeaterCoolerState - if (this.CurrentHeaterCoolerState === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} CurrentHeaterCoolerState: ${this.CurrentHeaterCoolerState}`); + if (this.HeaterCooler.CurrentHeaterCoolerState === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` CurrentHeaterCoolerState: ${this.HeaterCooler.CurrentHeaterCoolerState}`); } else { - this.accessory.context.CurrentHeaterCoolerState = this.CurrentHeaterCoolerState; - this.coolerService?.updateCharacteristic(this.hap.Characteristic.CurrentHeaterCoolerState, this.CurrentHeaterCoolerState); - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` updateCharacteristic CurrentHeaterCoolerState: ${this.CurrentHeaterCoolerState}`, - ); + this.accessory.context.CurrentHeaterCoolerState = this.HeaterCooler.CurrentHeaterCoolerState; + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.CurrentHeaterCoolerState, this.HeaterCooler.CurrentHeaterCoolerState); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` updateCharacteristic CurrentHeaterCoolerState: ${this.HeaterCooler.CurrentHeaterCoolerState}`); } // ThresholdTemperature - if (this.ThresholdTemperature === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} ThresholdTemperature: ${this.ThresholdTemperature}`); - } else { - this.accessory.context.ThresholdTemperature = this.ThresholdTemperature; - this.coolerService?.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, this.ThresholdTemperature); - this.coolerService?.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, this.ThresholdTemperature); - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + ` updateCharacteristic ThresholdTemperature: ${this.ThresholdTemperature}`, - ); - } - } - - async disablePushOnChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOn === undefined) { - this.disablePushOn = false; - } else { - this.disablePushOn = device.disablePushOn; - } - } - - async disablePushOffChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOff === undefined) { - this.disablePushOff = false; - } else { - this.disablePushOff = device.disablePushOff; - } - } - - async disablePushDetailChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushDetail === undefined) { - this.disablePushDetail = false; - } else { - this.disablePushDetail = device.disablePushDetail; - } - } - - async commandType(): Promise { - let commandType: string; - if (this.device.customize) { - commandType = 'customize'; - } else { - commandType = 'command'; - } - return commandType; - } - - async commandOn(): Promise { - let command: string; - if (this.device.customize && this.device.customOn) { - command = this.device.customOn; - } else { - command = 'turnOn'; - } - return command; - } - - async commandOff(): Promise { - let command: string; - if (this.device.customize && this.device.customOff) { - command = this.device.customOff; + if (this.HeaterCooler.ThresholdTemperature === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} ThresholdTemperature: ${this.HeaterCooler.ThresholdTemperature}`); } else { - command = 'turnOff'; - } - return command; - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - break; - case 171: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - break; - case 190: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, but ` - + `the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.remoteType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); + this.accessory.context.ThresholdTemperature = this.HeaterCooler.ThresholdTemperature; + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, this.HeaterCooler.ThresholdTemperature); + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, this.HeaterCooler.ThresholdTemperature); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` ThresholdTemperature: ${this.HeaterCooler.ThresholdTemperature}`); } } async apiError({ e }: { e: any }): Promise { - this.coolerService.updateCharacteristic(this.hap.Characteristic.Active, e); - this.coolerService.updateCharacteristic(this.hap.Characteristic.RotationSpeed, e); - this.coolerService.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); - this.coolerService.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); - this.coolerService.updateCharacteristic(this.hap.Characteristic.TargetHeaterCoolerState, e); - this.coolerService.updateCharacteristic(this.hap.Characteristic.CurrentHeaterCoolerState, e); - this.coolerService.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, e); - this.coolerService.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, e); + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.Active, e); + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, e); + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.TargetHeaterCoolerState, e); + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.CurrentHeaterCoolerState, e); + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, e); + this.HeaterCooler.Service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, e); } - async deviceContext(): Promise { - if (this.Active === undefined) { - this.Active = this.hap.Characteristic.Active.INACTIVE; - } else if (this.Active) { - this.Active; - } else { - this.Active = this.accessory.context.Active; - } - - if (this.CurrentTemperature === undefined && this.accessory.context.CurrentTemperature === undefined) { - this.CurrentTemperature = 24; - } else { - this.CurrentTemperature = this.CurrentTemperature || this.accessory.context.CurrentTemperature; - } - - if (this.ThresholdTemperature === undefined && this.accessory.context.ThresholdTemperature === undefined) { - this.ThresholdTemperature = 24; - } else { - this.ThresholdTemperature = this.ThresholdTemperature || this.accessory.context.ThresholdTemperature; - } - - if (this.RotationSpeed === undefined && this.accessory.context.RotationSpeed === undefined) { - this.RotationSpeed = 4; - } else { - this.RotationSpeed = this.RotationSpeed || this.accessory.context.RotationSpeed; - } - + async getAirConditionerConfigSettings(accessory: PlatformAccessory, device: irdevice & irDevicesConfig): Promise { if (this.device.irair?.hide_automode) { - this.hide_automode = this.device.irair?.hide_automode; - this.accessory.context.hide_automode = this.hide_automode; + this.hide_automode = device.irair?.hide_automode; + accessory.context.hide_automode = this.hide_automode; } else { - this.hide_automode = this.device.irair?.hide_automode; - this.accessory.context.hide_automode = this.hide_automode; + this.hide_automode = device.irair?.hide_automode; + accessory.context.hide_automode = this.hide_automode; } if (this.device.irair?.set_max_heat) { - this.set_max_heat = this.device.irair?.set_max_heat; - this.accessory.context.set_max_heat = this.set_max_heat; + this.set_max_heat = device.irair?.set_max_heat; + accessory.context.set_max_heat = this.set_max_heat; } else { this.set_max_heat = 35; - this.accessory.context.set_max_heat = this.set_max_heat; + accessory.context.set_max_heat = this.set_max_heat; } if (this.device.irair?.set_min_heat) { - this.set_min_heat = this.device.irair?.set_min_heat; - this.accessory.context.set_min_heat = this.set_min_heat; + this.set_min_heat = device.irair?.set_min_heat; + accessory.context.set_min_heat = this.set_min_heat; } else { this.set_min_heat = 0; - this.accessory.context.set_min_heat = this.set_min_heat; + accessory.context.set_min_heat = this.set_min_heat; } if (this.device.irair?.set_max_cool) { - this.set_max_cool = this.device.irair?.set_max_cool; - this.accessory.context.set_max_cool = this.set_max_cool; + this.set_max_cool = device.irair?.set_max_cool; + accessory.context.set_max_cool = this.set_max_cool; } else { this.set_max_cool = 35; - this.accessory.context.set_max_cool = this.set_max_cool; + accessory.context.set_max_cool = this.set_max_cool; } if (this.device.irair?.set_min_cool) { - this.set_min_cool = this.device.irair?.set_min_cool; - this.accessory.context.set_min_cool = this.set_min_cool; + this.set_min_cool = device.irair?.set_min_cool; + accessory.context.set_min_cool = this.set_min_cool; } else { this.set_min_cool = 0; - this.accessory.context.set_min_cool = this.set_min_cool; - } - - if (this.meter) { - if (this.CurrentRelativeHumidity === undefined && this.accessory.context.CurrentRelativeHumidity === undefined) { - this.CurrentRelativeHumidity = 0; - } else { - this.CurrentRelativeHumidity = this.CurrentRelativeHumidity || this.accessory.context.CurrentRelativeHumidity; - } - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async deviceConfig(device: irdevice & irDevicesConfig): Promise { - let config = {}; - if (device.irair) { - config = device.irair; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.customOn !== undefined) { - config['customOn'] = device.customOn; - } - if (device.customOff !== undefined) { - config['customOff'] = device.customOff; - } - if (device.customize !== undefined) { - config['customize'] = device.customize; - } - if (device.disablePushOn !== undefined) { - config['disablePushOn'] = device.disablePushOn; - } - if (device.disablePushOff !== undefined) { - config['disablePushOff'] = device.disablePushOff; - } - if (device.disablePushDetail !== undefined) { - config['disablePushDetail'] = device.disablePushDetail; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: irdevice & irDevicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging() && this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging() && this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); + accessory.context.set_min_cool = this.set_min_cool; } } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; - } } diff --git a/src/irdevice/airpurifier.ts b/src/irdevice/airpurifier.ts index 6669aa94..cb61640d 100644 --- a/src/irdevice/airpurifier.ts +++ b/src/irdevice/airpurifier.ts @@ -1,31 +1,42 @@ -import { CharacteristicValue, PlatformAccessory, Service, API, Logging, HAP } from 'homebridge'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * airpurifier.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, irDevicesConfig, irdevice, SwitchBotPlatformConfig } from '../settings.js'; +import { Devices } from '../settings.js'; +import { irdeviceBase } from './irdevice.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { irDevicesConfig, irdevice } from '../settings.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class AirPurifier { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class AirPurifier extends irdeviceBase { // Services - airPurifierService!: Service; + private AirPurifier: { + Name: CharacteristicValue; + Service: Service; + Active: CharacteristicValue; + RotationSpeed: CharacteristicValue; + CurrentAirPurifierState: CharacteristicValue; + TargetAirPurifierState: CharacteristicValue; + }; + + private TemperatureSensor: { + Name: CharacteristicValue; + Service: Service; + CurrentTemperature: CharacteristicValue; + }; // Characteristic Values - Active!: CharacteristicValue; APActive!: CharacteristicValue; CurrentAPTemp!: CharacteristicValue; CurrentAPMode!: CharacteristicValue; - RotationSpeed!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; CurrentAPFanSpeed!: CharacteristicValue; - CurrentTemperature!: CharacteristicValue; - CurrentAirPurifierState!: CharacteristicValue; CurrentHeaterCoolerState!: CharacteristicValue; // Others @@ -38,60 +49,68 @@ export class AirPurifier { CurrentFanSpeed!: number; static PURIFYING_AIR: number; - // Config - disablePushOn?: boolean; - disablePushOff?: boolean; - deviceLogging!: string; - constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: irdevice & irDevicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: irdevice & irDevicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // default placeholders - this.deviceLogs(device); - this.deviceContext(); - this.disablePushOnChanges(device); - this.disablePushOffChanges(device); - this.deviceConfig(device); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, device.remoteType) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Television service if it exists, otherwise create a new Television service - // you can create multiple services for each accessory - const airPurifierService = `${accessory.displayName} Air Purifier`; - (this.airPurifierService = accessory.getService(this.hap.Service.AirPurifier) - || accessory.addService(this.hap.Service.AirPurifier)), airPurifierService; - - this.airPurifierService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.airPurifierService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.airPurifierService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - // handle on / off events using the Active characteristic - this.airPurifierService.getCharacteristic(this.hap.Characteristic.Active).onSet(this.ActiveSet.bind(this)); - - this.airPurifierService.getCharacteristic(this.hap.Characteristic.CurrentAirPurifierState).onGet(() => { - return this.CurrentAirPurifierStateGet(); - }); + super(platform, accessory, device); + + // Initialize AirPurifier Service + accessory.context.AirPurifier = accessory.context.AirPurifier ?? {}; + this.AirPurifier = { + Name: accessory.context.AirPurifier.Name ?? `${accessory.displayName} Air Purifier`, + Service: accessory.getService(this.hap.Service.AirPurifier) ?? accessory.addService(this.hap.Service.AirPurifier) as Service, + Active: accessory.context.Active ?? this.hap.Characteristic.Active.INACTIVE, + RotationSpeed: accessory.context.RotationSpeed ?? 0, + CurrentAirPurifierState: accessory.context.CurrentAirPurifierState ?? this.hap.Characteristic.CurrentAirPurifierState.INACTIVE, + TargetAirPurifierState: accessory.context.TargetAirPurifierState ?? this.hap.Characteristic.TargetAirPurifierState.AUTO, + }; + accessory.context.AirPurifier = this.AirPurifier as object; + + this.AirPurifier.Service + .setCharacteristic(this.hap.Characteristic.Name, this.AirPurifier.Name) + .getCharacteristic(this.hap.Characteristic.Active) + .onGet(() => { + return this.AirPurifier.Active; + }) + .onSet(this.ActiveSet.bind(this)); + + this.AirPurifier.Service + .getCharacteristic(this.hap.Characteristic.CurrentAirPurifierState) + .onGet(() => { + return this.CurrentAirPurifierStateGet(); + }); - this.airPurifierService.getCharacteristic(this.hap.Characteristic.TargetAirPurifierState).onSet(this.TargetAirPurifierStateSet.bind(this)); + this.AirPurifier.Service + .getCharacteristic(this.hap.Characteristic.TargetAirPurifierState) + .onGet(() => { + return this.AirPurifier.TargetAirPurifierState; + }) + .onSet(this.TargetAirPurifierStateSet.bind(this)); + + // Initialize TemperatureSensor Service + accessory.context.TemperatureSensor = accessory.context.TemperatureSensor ?? {}; + this.TemperatureSensor = { + Name: accessory.context.TemperatureSensor.Name ?? `${accessory.displayName} Temperature Sensor`, + Service: accessory.getService(this.hap.Service.TemperatureSensor) ?? accessory.addService(this.hap.Service.TemperatureSensor) as Service, + CurrentTemperature: accessory.context.CurrentTemperature || 24, + }; + accessory.context.TemperatureSensor = this.TemperatureSensor as object; + + this.TemperatureSensor.Service + .setCharacteristic(this.hap.Characteristic.Name, this.TemperatureSensor.Name) + .getCharacteristic(this.hap.Characteristic.CurrentTemperature) + .onGet(() => { + return this.TemperatureSensor.CurrentTemperature; + }); } async ActiveSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Set Active: ${value}`); - this.Active = value; - if (this.Active === this.hap.Characteristic.Active.ACTIVE) { + this.AirPurifier.Active = value; + if (this.AirPurifier.Active === this.hap.Characteristic.Active.ACTIVE) { this.pushAirPurifierOnChanges(); } else { this.pushAirPurifierOffChanges(); @@ -115,12 +134,12 @@ export class AirPurifier { } async CurrentAirPurifierStateGet(): Promise { - if (this.Active === 1) { - this.CurrentAirPurifierState = this.hap.Characteristic.CurrentAirPurifierState.PURIFYING_AIR; + if (this.AirPurifier.Active === 1) { + this.AirPurifier.CurrentAirPurifierState = this.hap.Characteristic.CurrentAirPurifierState.PURIFYING_AIR; } else { - this.CurrentAirPurifierState = this.hap.Characteristic.CurrentAirPurifierState.INACTIVE; + this.AirPurifier.CurrentAirPurifierState = this.hap.Characteristic.CurrentAirPurifierState.INACTIVE; } - return this.CurrentAirPurifierState; + return this.AirPurifier.CurrentAirPurifierState; } /** @@ -135,11 +154,9 @@ export class AirPurifier { * AirPurifier: "command" "highSpeed" "default" = fan speed to high */ async pushAirPurifierOnChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushAirPurifierOnChanges Active: ${this.Active},` + - ` disablePushOn: ${this.disablePushOn}`, - ); - if (this.Active === this.hap.Characteristic.Active.ACTIVE && !this.disablePushOn) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirPurifierOnChanges Active: ${this.AirPurifier.Active},` + + ` disablePushOn: ${this.disablePushOn}`); + if (this.AirPurifier.Active === this.hap.Characteristic.Active.ACTIVE && !this.disablePushOn) { const commandType: string = await this.commandType(); const command: string = await this.commandOn(); const bodyChange = JSON.stringify({ @@ -152,11 +169,9 @@ export class AirPurifier { } async pushAirPurifierOffChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushAirPurifierOffChanges Active: ${this.Active},` + - ` disablePushOff: ${this.disablePushOff}`, - ); - if (this.Active === this.hap.Characteristic.Active.INACTIVE && !this.disablePushOn) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirPurifierOffChanges Active: ${this.AirPurifier.Active},` + + ` disablePushOff: ${this.disablePushOff}`); + if (this.AirPurifier.Active === this.hap.Characteristic.Active.INACTIVE && !this.disablePushOn) { const commandType: string = await this.commandType(); const command: string = await this.commandOff(); const bodyChange = JSON.stringify({ @@ -169,10 +184,8 @@ export class AirPurifier { } async pushAirPurifierStatusChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushAirPurifierStatusChanges Active: ${this.Active},` + - ` disablePushOff: ${this.disablePushOff}, disablePushOn: ${this.disablePushOn}`, - ); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirPurifierStatusChanges Active: ${this.AirPurifier.Active},` + + ` disablePushOff: ${this.disablePushOff}, disablePushOn: ${this.disablePushOn}`); if (!this.Busy) { this.Busy = true; this.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.IDLE; @@ -184,22 +197,20 @@ export class AirPurifier { } async pushAirPurifierDetailsChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushAirPurifierDetailsChanges Active: ${this.Active},` + - ` disablePushOff: ${this.disablePushOff}, disablePushOn: ${this.disablePushOn}`, - ); - this.CurrentAPTemp = this.CurrentTemperature || 24; - this.CurrentAPMode = this.CurrentMode || 1; - this.CurrentAPFanSpeed = this.CurrentFanSpeed || 1; - this.APActive = this.Active === 1 ? 'on' : 'off'; + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushAirPurifierDetailsChanges Active: ${this.AirPurifier.Active},` + + ` disablePushOff: ${this.disablePushOff}, disablePushOn: ${this.disablePushOn}`); + this.CurrentAPTemp = this.TemperatureSensor!.CurrentTemperature ?? 24; + this.CurrentAPMode = this.CurrentMode ?? 1; + this.CurrentAPFanSpeed = this.CurrentFanSpeed ?? 1; + this.APActive = this.AirPurifier.Active === 1 ? 'on' : 'off'; const parameter = `${this.CurrentAPTemp},${this.CurrentAPMode},${this.CurrentAPFanSpeed},${this.APActive}`; const bodyChange = JSON.stringify({ command: 'setAll', parameter: `${parameter}`, commandType: 'command', }); - if (this.Active === 1) { - if ((Number(this.CurrentTemperature) || 24) < (this.LastTemperature || 30)) { + if (this.AirPurifier.Active === 1) { + if ((Number(this.TemperatureSensor!.CurrentTemperature) || 24) < (this.LastTemperature || 30)) { this.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.COOLING; } else { this.CurrentHeaterCoolerState = this.hap.Characteristic.CurrentHeaterCoolerState.HEATING; @@ -225,8 +236,10 @@ export class AirPurifier { this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -234,295 +247,58 @@ export class AirPurifier { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.warnLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`, - ); + this.warnLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`); } } async updateHomeKitCharacteristics(): Promise { // Active - if (this.Active === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${this.Active}`); + if (this.AirPurifier.Active === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${this.AirPurifier.Active}`); } else { - this.accessory.context.Active = this.Active; - this.airPurifierService?.updateCharacteristic(this.hap.Characteristic.Active, this.Active); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Active}`); + this.accessory.context.Active = this.AirPurifier.Active; + this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.Active, this.AirPurifier.Active); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.AirPurifier.Active}`); } // CurrentAirPurifierState - if (this.CurrentAirPurifierState === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} CurrentAirPurifierState: ${this.CurrentAirPurifierState}`); + if (this.AirPurifier.CurrentAirPurifierState === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} CurrentAirPurifierState: ${this.AirPurifier.CurrentAirPurifierState}`); } else { - this.accessory.context.CurrentAirPurifierState = this.CurrentAirPurifierState; - this.airPurifierService?.updateCharacteristic(this.hap.Characteristic.CurrentAirPurifierState, this.CurrentAirPurifierState); - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + ` updateCharacteristic CurrentAirPurifierState: ${this.CurrentAirPurifierState}`, - ); + this.accessory.context.CurrentAirPurifierState = this.AirPurifier.CurrentAirPurifierState; + this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.CurrentAirPurifierState, this.AirPurifier.CurrentAirPurifierState); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentAirPurifierState: ${this.AirPurifier.CurrentAirPurifierState}`); } // CurrentHeaterCoolerState if (this.CurrentHeaterCoolerState === undefined) { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} CurrentHeaterCoolerState: ${this.CurrentHeaterCoolerState}`); } else { this.accessory.context.CurrentHeaterCoolerState = this.CurrentHeaterCoolerState; - this.airPurifierService?.updateCharacteristic(this.hap.Characteristic.CurrentHeaterCoolerState, this.CurrentHeaterCoolerState); - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` updateCharacteristic CurrentHeaterCoolerState: ${this.CurrentHeaterCoolerState}`, - ); - } - } - - async disablePushOnChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOn === undefined) { - this.disablePushOn = false; - } else { - this.disablePushOn = device.disablePushOn; - } - } - - async disablePushOffChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOff === undefined) { - this.disablePushOff = false; - } else { - this.disablePushOff = device.disablePushOff; - } - } - - async commandType(): Promise { - let commandType: string; - if (this.device.customize) { - commandType = 'customize'; - } else { - commandType = 'command'; - } - return commandType; - } - - async commandOn(): Promise { - let command: string; - if (this.device.customize && this.device.customOn) { - command = this.device.customOn; - } else { - command = 'turnOn'; + this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.CurrentHeaterCoolerState, this.CurrentHeaterCoolerState); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentHeaterCoolerState: ${this.CurrentHeaterCoolerState}`); } - return command; - } - - async commandOff(): Promise { - let command: string; - if (this.device.customize && this.device.customOff) { - command = this.device.customOff; + // CurrentTemperature + if (this.TemperatureSensor.CurrentTemperature === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} CurrentTemperature: ${this.TemperatureSensor.CurrentTemperature}`); } else { - command = 'turnOff'; - } - return command; - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - break; - case 171: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - break; - case 190: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, but ` - + `the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.remoteType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); + this.accessory.context.CurrentTemperature = this.TemperatureSensor.CurrentTemperature; + this.TemperatureSensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.TemperatureSensor.CurrentTemperature); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentTemperature: ${this.TemperatureSensor.CurrentTemperature}`); } } async apiError(e: any): Promise { - this.airPurifierService.updateCharacteristic(this.hap.Characteristic.CurrentHeaterCoolerState, e); - this.airPurifierService.updateCharacteristic(this.hap.Characteristic.CurrentAirPurifierState, e); - this.airPurifierService.updateCharacteristic(this.hap.Characteristic.TargetAirPurifierState, e); - this.airPurifierService.updateCharacteristic(this.hap.Characteristic.Active, e); - } - - private deviceContext() { - if (this.Active === undefined) { - this.Active = this.hap.Characteristic.Active.INACTIVE; - } else { - this.Active = this.accessory.context.Active; - } - if (this.CurrentTemperature === undefined) { - this.CurrentTemperature = 24; - } else { - this.CurrentTemperature = this.accessory.context.CurrentTemperature; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async deviceConfig(device: irdevice & irDevicesConfig): Promise { - let config = {}; - if (device.irpur) { - config = device.irpur; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.customOn !== undefined) { - config['customOn'] = device.customOn; - } - if (device.customOff !== undefined) { - config['customOff'] = device.customOff; - } - if (device.customize !== undefined) { - config['customize'] = device.customize; - } - if (device.disablePushOn !== undefined) { - config['disablePushOn'] = device.disablePushOn; - } - if (device.disablePushOff !== undefined) { - config['disablePushOff'] = device.disablePushOff; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: irdevice & irDevicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.CurrentHeaterCoolerState, e); + this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.CurrentAirPurifierState, e); + this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.TargetAirPurifierState, e); + this.AirPurifier.Service.updateCharacteristic(this.hap.Characteristic.Active, e); + this.TemperatureSensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); } } diff --git a/src/irdevice/camera.ts b/src/irdevice/camera.ts index 0283077e..b1035c02 100644 --- a/src/irdevice/camera.ts +++ b/src/irdevice/camera.ts @@ -1,74 +1,58 @@ -import { CharacteristicValue, PlatformAccessory, Service, API, Logging, HAP } from 'homebridge'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * camera.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, irDevicesConfig, irdevice, SwitchBotPlatformConfig } from '../settings.js'; +import { Devices } from '../settings.js'; +import { irdeviceBase } from './irdevice.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { irDevicesConfig, irdevice } from '../settings.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class Camera { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class Camera extends irdeviceBase { // Services - switchService!: Service; - - // Characteristic Values - On!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - - // Config - deviceLogging!: string; - disablePushOn?: boolean; - disablePushOff?: boolean; + private Switch: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: irdevice & irDevicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: irdevice & irDevicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // default placeholders - this.deviceLogs(device); - this.deviceContext(); - this.disablePushOnChanges(device); - this.disablePushOffChanges(device); - this.deviceConfig(device); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, device.remoteType) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Television service if it exists, otherwise create a new Television service - // you can create multiple services for each accessory - const switchService = `${accessory.displayName} Camera`; - (this.switchService = accessory.getService(this.hap.Service.Switch) - || accessory.addService(this.hap.Service.Switch)), switchService; - - this.switchService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.switchService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.switchService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - - // handle on / off events using the On characteristic - this.switchService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); + super(platform, accessory, device); + + // Initialize Switch Service + accessory.context.Switch = accessory.context.Switch ?? {}; + this.Switch = { + Name: accessory.context.Switch.Name ?? `${accessory.displayName} Camera`, + Service: accessory.getService(this.hap.Service.Switch) ?? accessory.addService(this.hap.Service.Switch) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Switch = this.Switch as object; + + this.Switch.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Switch.Name) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.Switch.On; + }) + .onSet(this.OnSet.bind(this)); } async OnSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${value}`); - this.On = value; - if (this.On) { + this.Switch.On = value; + if (this.Switch.On) { this.pushOnChanges(); } else { this.pushOffChanges(); @@ -86,8 +70,9 @@ export class Camera { * Camera - "command" "channelSub" "default" = previous channel */ async pushOnChanges(): Promise { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushOnChanges On: ${this.On},` + ` disablePushOn: ${this.disablePushOn}`); - if (this.On && !this.disablePushOn) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushOnChanges On: ${this.Switch.On},` + + ` disablePushOn: ${this.disablePushOn}`); + if (this.Switch.On && !this.disablePushOn) { const commandType: string = await this.commandType(); const command: string = await this.commandOn(); const bodyChange = JSON.stringify({ @@ -100,10 +85,9 @@ export class Camera { } async pushOffChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushOffChanges On: ${this.On},` + ` disablePushOff: ${this.disablePushOff}`, - ); - if (!this.On && !this.disablePushOff) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushOffChanges On: ${this.Switch.On},` + + ` disablePushOff: ${this.disablePushOff}`); + if (!this.Switch.On && !this.disablePushOff) { const commandType: string = await this.commandType(); const command: string = await this.commandOff(); const bodyChange = JSON.stringify({ @@ -130,8 +114,10 @@ export class Camera { this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -139,266 +125,27 @@ export class Camera { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.warnLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`, - ); + this.warnLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`); } } async updateHomeKitCharacteristics(): Promise { // On - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); - } else { - this.accessory.context.On = this.On; - this.switchService?.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); - } - } - - async disablePushOnChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOn === undefined) { - this.disablePushOn = false; - } else { - this.disablePushOn = device.disablePushOn; - } - } - - async disablePushOffChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOff === undefined) { - this.disablePushOff = false; + if (this.Switch.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.Switch.On}`); } else { - this.disablePushOff = device.disablePushOff; - } - } - - async commandType(): Promise { - let commandType: string; - if (this.device.customize) { - commandType = 'customize'; - } else { - commandType = 'command'; - } - return commandType; - } - - async commandOn(): Promise { - let command: string; - if (this.device.customize && this.device.customOn) { - command = this.device.customOn; - } else { - command = 'turnOn'; - } - return command; - } - - async commandOff(): Promise { - let command: string; - if (this.device.customize && this.device.customOff) { - command = this.device.customOff; - } else { - command = 'turnOff'; - } - return command; - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - break; - case 171: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - break; - case 190: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, but ` - + `the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.remoteType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); + this.accessory.context.On = this.Switch.On; + this.Switch.Service.updateCharacteristic(this.hap.Characteristic.On, this.Switch.On); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.Switch.On}`); } } async apiError(e: any): Promise { - this.switchService.updateCharacteristic(this.hap.Characteristic.On, e); - } - - async deviceContext() { - if (this.On === undefined) { - this.On = false; - } else { - this.On = this.accessory.context.On; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async deviceConfig(device: irdevice & irDevicesConfig): Promise { - let config = {}; - if (device.ircam) { - config = device.ircam; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.customOn !== undefined) { - config['customOn'] = device.customOn; - } - if (device.customOff !== undefined) { - config['customOff'] = device.customOff; - } - if (device.customize !== undefined) { - config['customize'] = device.customize; - } - if (device.disablePushOn !== undefined) { - config['disablePushOn'] = device.disablePushOn; - } - if (device.disablePushOff !== undefined) { - config['disablePushOff'] = device.disablePushOff; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: irdevice & irDevicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.Switch.Service.updateCharacteristic(this.hap.Characteristic.On, e); } } diff --git a/src/irdevice/fan.ts b/src/irdevice/fan.ts index 8f9c80eb..95238c69 100644 --- a/src/irdevice/fan.ts +++ b/src/irdevice/fan.ts @@ -1,162 +1,132 @@ -import { CharacteristicValue, PlatformAccessory, Service, API, Logging, HAP } from 'homebridge'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * fan.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, irDevicesConfig, irdevice, SwitchBotPlatformConfig } from '../settings.js'; +import { Devices } from '../settings.js'; +import { irdeviceBase } from './irdevice.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { irDevicesConfig, irdevice } from '../settings.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class Fan { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class IRFan extends irdeviceBase { // Services - fanService!: Service; - - // Characteristic Values - Active!: CharacteristicValue; - SwingMode!: CharacteristicValue; - RotationSpeed!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - ActiveIdentifier!: CharacteristicValue; - RotationDirection!: CharacteristicValue; - - // Config - minStep?: number; - minValue?: number; - maxValue?: number; - deviceLogging!: string; - disablePushOn?: boolean; - disablePushOff?: boolean; + private Fan: { + Name: CharacteristicValue; + Service: Service; + Active: CharacteristicValue; + SwingMode: CharacteristicValue; + RotationSpeed: CharacteristicValue; + RotationDirection: CharacteristicValue; + }; constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: irdevice & irDevicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: irdevice & irDevicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // default placeholders - this.deviceLogs(device); - this.deviceContext(); - this.disablePushOnChanges(device); - this.disablePushOffChanges(device); - this.deviceConfig(device); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, device.remoteType) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Television service if it exists, otherwise create a new Television service - // you can create multiple services for each accessory - const fanService = `${accessory.displayName} Fan`; - (this.fanService = accessory.getService(this.hap.Service.Fanv2) - || accessory.addService(this.hap.Service.Fanv2)), fanService; - - this.fanService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.fanService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.fanService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - - // handle on / off events using the Active characteristic - this.fanService.getCharacteristic(this.hap.Characteristic.Active).onSet(this.ActiveSet.bind(this)); + super(platform, accessory, device); + + // Initialize Switch Service + accessory.context.Fan = accessory.context.Fan ?? {}; + this.Fan = { + Name: accessory.context.Fan.Name ?? `${accessory.displayName} Fan`, + Service: accessory.getService(this.hap.Service.Fanv2) ?? accessory.addService(this.hap.Service.Fanv2) as Service, + Active: accessory.context.Active ?? this.hap.Characteristic.Active.INACTIVE, + SwingMode: accessory.context.SwingMode ?? this.hap.Characteristic.SwingMode.SWING_DISABLED, + RotationSpeed: accessory.context.RotationSpeed ?? 0, + RotationDirection: accessory.context.RotationDirection ?? this.hap.Characteristic.RotationDirection.CLOCKWISE, + }; + accessory.context.Fan = this.Fan as object; + + this.Fan.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Fan.Name) + .getCharacteristic(this.hap.Characteristic.Active) + .onGet(() => { + return this.Fan.Active; + }) + .onSet(this.ActiveSet.bind(this)); if (device.irfan?.rotation_speed) { - if (device.irfan?.set_minStep) { - this.minStep = device.irfan?.set_minStep; - } else { - this.minStep = 1; - } - if (device.irfan?.set_min) { - this.minValue = device.irfan?.set_min; - } else { - this.minValue = 1; - } - if (device.irfan?.set_max) { - this.maxValue = device.irfan?.set_max; - } else { - this.maxValue = 100; - } // handle Rotation Speed events using the RotationSpeed characteristic - this.fanService + this.Fan.Service .getCharacteristic(this.hap.Characteristic.RotationSpeed) .setProps({ - minStep: this.minStep, - minValue: this.minValue, - maxValue: this.maxValue, + minStep: device.irfan?.set_minStep ?? 1, + minValue: device.irfan?.set_min ?? 1, + maxValue: device.irfan?.set_max ?? 100, + }) + .onGet(() => { + return this.Fan.RotationSpeed; }) .onSet(this.RotationSpeedSet.bind(this)); - } else if (this.fanService.testCharacteristic(this.hap.Characteristic.RotationSpeed) && !device.irfan?.swing_mode) { - const characteristic = this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed); - this.fanService.removeCharacteristic(characteristic); + } else if (this.Fan.Service.testCharacteristic(this.hap.Characteristic.RotationSpeed) && !device.irfan?.swing_mode) { + const characteristic = this.Fan.Service.getCharacteristic(this.hap.Characteristic.RotationSpeed); + this.Fan.Service.removeCharacteristic(characteristic); this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Rotation Speed Characteristic was removed.`); } else { - // eslint-disable-next-line max-len - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} RotationSpeed Characteristic was not removed/added, ` + - `Clear Cache on ${this.accessory.displayName} to remove Chracteristic`, - ); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} RotationSpeed Characteristic was not removed/added, ` + + `Clear Cache on ${this.accessory.displayName} to remove Chracteristic`); } if (device.irfan?.swing_mode) { // handle Osolcation events using the SwingMode characteristic - this.fanService.getCharacteristic(this.hap.Characteristic.SwingMode).onSet(this.SwingModeSet.bind(this)); - } else if (this.fanService.testCharacteristic(this.hap.Characteristic.SwingMode) && !device.irfan?.swing_mode) { - const characteristic = this.fanService.getCharacteristic(this.hap.Characteristic.SwingMode); - this.fanService.removeCharacteristic(characteristic); + this.Fan.Service + .getCharacteristic(this.hap.Characteristic.SwingMode) + .onGet(() => { + return this.Fan.SwingMode; + }) + .onSet(this.SwingModeSet.bind(this)); + } else if (this.Fan.Service.testCharacteristic(this.hap.Characteristic.SwingMode) && !device.irfan?.swing_mode) { + const characteristic = this.Fan.Service.getCharacteristic(this.hap.Characteristic.SwingMode); + this.Fan.Service.removeCharacteristic(characteristic); this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Swing Mode Characteristic was removed.`); } else { - // eslint-disable-next-line max-len - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} Swing Mode Characteristic was not removed/added, ` + - `Clear Cache on ${this.accessory.displayName} To Remove Chracteristic`, - ); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Swing Mode Characteristic was not removed/added, ` + + `Clear Cache on ${this.accessory.displayName} To Remove Chracteristic`); } } async SwingModeSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} SwingMode: ${value}`); - if (value > this.SwingMode) { - this.SwingMode = 1; + if (value > this.Fan.SwingMode) { + this.Fan.SwingMode = 1; await this.pushFanOnChanges(); await this.pushFanSwingChanges(); } else { - this.SwingMode = 0; + this.Fan.SwingMode = 0; await this.pushFanOnChanges(); await this.pushFanSwingChanges(); } - this.SwingMode = value; - this.accessory.context.SwingMode = this.SwingMode; + this.Fan.SwingMode = value; + this.accessory.context.SwingMode = this.Fan.SwingMode; } async RotationSpeedSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} RotationSpeed: ${value}`); - if (value > this.RotationSpeed) { - this.RotationSpeed = 1; + if (value > this.Fan.RotationSpeed) { + this.Fan.RotationSpeed = 1; this.pushFanSpeedUpChanges(); this.pushFanOnChanges(); } else { - this.RotationSpeed = 0; + this.Fan.RotationSpeed = 0; this.pushFanSpeedDownChanges(); } - this.RotationSpeed = value; - this.accessory.context.RotationSpeed = this.RotationSpeed; + this.Fan.RotationSpeed = value; + this.accessory.context.RotationSpeed = this.Fan.RotationSpeed; } async ActiveSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${value}`); - this.Active = value; - if (this.Active === this.hap.Characteristic.Active.ACTIVE) { + this.Fan.Active = value; + if (this.Fan.Active === this.hap.Characteristic.Active.ACTIVE) { this.pushFanOnChanges(); } else { this.pushFanOffChanges(); @@ -173,10 +143,9 @@ export class Fan { * Fan - "command" "highSpeed" "default" = fan speed to high */ async pushFanOnChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushFanOnChanges Active: ${this.Active},` + ` disablePushOn: ${this.disablePushOn}`, - ); - if (this.Active === this.hap.Characteristic.Active.ACTIVE && !this.disablePushOn) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushFanOnChanges Active: ${this.Fan.Active},` + + ` disablePushOn: ${this.disablePushOn}`); + if (this.Fan.Active === this.hap.Characteristic.Active.ACTIVE && !this.disablePushOn) { const commandType: string = await this.commandType(); const command: string = await this.commandOn(); const bodyChange = JSON.stringify({ @@ -189,11 +158,9 @@ export class Fan { } async pushFanOffChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushLightOffChanges Active: ${this.Active},` + - ` disablePushOff: ${this.disablePushOff}`, - ); - if (this.Active === this.hap.Characteristic.Active.INACTIVE && !this.disablePushOff) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushLightOffChanges Active: ${this.Fan.Active},` + + ` disablePushOff: ${this.disablePushOff}`); + if (this.Fan.Active === this.hap.Characteristic.Active.INACTIVE && !this.disablePushOff) { const commandType: string = await this.commandType(); const command: string = await this.commandOff(); const bodyChange = JSON.stringify({ @@ -247,8 +214,10 @@ export class Fan { this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -256,281 +225,42 @@ export class Fan { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.warnLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`, - ); + this.warnLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`); } } async updateHomeKitCharacteristics(): Promise { - if (this.Active === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${this.Active}`); - } else { - this.accessory.context.Active = this.Active; - this.fanService?.updateCharacteristic(this.hap.Characteristic.Active, this.Active); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Active}`); - } - if (this.SwingMode === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} SwingMode: ${this.SwingMode}`); - } else { - this.accessory.context.SwingMode = this.SwingMode; - this.fanService?.updateCharacteristic(this.hap.Characteristic.SwingMode, this.SwingMode); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic SwingMode: ${this.SwingMode}`); - } - if (this.RotationSpeed === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} RotationSpeed: ${this.RotationSpeed}`); - } else { - this.accessory.context.RotationSpeed = this.RotationSpeed; - this.fanService?.updateCharacteristic(this.hap.Characteristic.RotationSpeed, this.RotationSpeed); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic RotationSpeed: ${this.RotationSpeed}`); - } - } - - async disablePushOnChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOn === undefined) { - this.disablePushOn = false; + if (this.Fan.Active === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${this.Fan.Active}`); } else { - this.disablePushOn = device.disablePushOn; + this.accessory.context.Active = this.Fan.Active; + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, this.Fan.Active); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Fan.Active}`); } - } - - async disablePushOffChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOff === undefined) { - this.disablePushOff = false; + if (this.Fan.SwingMode === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} SwingMode: ${this.Fan.SwingMode}`); } else { - this.disablePushOff = device.disablePushOff; + this.accessory.context.SwingMode = this.Fan.SwingMode; + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.SwingMode, this.Fan.SwingMode); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic SwingMode: ${this.Fan.SwingMode}`); } - } - - async commandType(): Promise { - let commandType: string; - if (this.device.customize) { - commandType = 'customize'; + if (this.Fan.RotationSpeed === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} RotationSpeed: ${this.Fan.RotationSpeed}`); } else { - commandType = 'command'; - } - return commandType; - } - - async commandOn(): Promise { - let command: string; - if (this.device.customize && this.device.customOn) { - command = this.device.customOn; - } else { - command = 'turnOn'; - } - return command; - } - - async commandOff(): Promise { - let command: string; - if (this.device.customize && this.device.customOff) { - command = this.device.customOff; - } else { - command = 'turnOff'; - } - return command; - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - break; - case 171: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - break; - case 190: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, but ` - + `the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.remoteType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); + this.accessory.context.RotationSpeed = this.Fan.RotationSpeed; + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, this.Fan.RotationSpeed); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic RotationSpeed: ${this.Fan.RotationSpeed}`); } } async apiError(e: any): Promise { - this.fanService.updateCharacteristic(this.hap.Characteristic.Active, e); - this.fanService.updateCharacteristic(this.hap.Characteristic.RotationSpeed, e); - this.fanService.updateCharacteristic(this.hap.Characteristic.SwingMode, e); - } - - async deviceContext() { - if (this.Active === undefined) { - this.Active = this.hap.Characteristic.Active.INACTIVE; - } else { - this.Active = this.accessory.context.Active; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async deviceConfig(device: irdevice & irDevicesConfig): Promise { - let config = {}; - if (device.irfan) { - config = device.irfan; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.customOn !== undefined) { - config['customOn'] = device.customOn; - } - if (device.customOff !== undefined) { - config['customOff'] = device.customOff; - } - if (device.customize !== undefined) { - config['customize'] = device.customize; - } - if (device.disablePushOn !== undefined) { - config['disablePushOn'] = device.disablePushOn; - } - if (device.disablePushOff !== undefined) { - config['disablePushOff'] = device.disablePushOff; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: irdevice & irDevicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, e); + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.RotationSpeed, e); + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.SwingMode, e); } } diff --git a/src/irdevice/irdevice.ts b/src/irdevice/irdevice.ts new file mode 100644 index 00000000..6508986f --- /dev/null +++ b/src/irdevice/irdevice.ts @@ -0,0 +1,382 @@ +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * device.ts: @switchbot/homebridge-switchbot. + */ +import type { SwitchBotPlatform } from '../platform.js'; +import type { API, HAP, Logging, PlatformAccessory } from 'homebridge'; +import type { SwitchBotPlatformConfig, irDevicesConfig, irdevice } from '../settings.js'; + +export abstract class irdeviceBase { + public readonly api: API; + public readonly log: Logging; + public readonly config!: SwitchBotPlatformConfig; + protected readonly hap: HAP; + + // Config + protected deviceLogging!: string; + protected disablePushOn!: boolean; + protected disablePushOff!: boolean; + protected disablePushDetail?: boolean; + + constructor( + protected readonly platform: SwitchBotPlatform, + protected accessory: PlatformAccessory, + protected device: irdevice & irDevicesConfig, + ) { + this.api = this.platform.api; + this.log = this.platform.log; + this.config = this.platform.config; + this.hap = this.api.hap; + + this.getDeviceLogSettings(device); + this.getDeviceConfigSettings(device); + this.getDeviceContext(accessory, device); + this.disablePushOnChanges(device); + this.disablePushOffChanges(device); + + // Set accessory information + accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') + .setCharacteristic(this.hap.Characteristic.AppMatchingIdentifier, 'id1087374760') + .setCharacteristic(this.hap.Characteristic.Name, accessory.context.name ?? accessory.displayName) + .setCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.context.name ?? accessory.displayName) + .setCharacteristic(this.hap.Characteristic.Model, accessory.context.model ?? 'Unknown') + .setCharacteristic(this.hap.Characteristic.ProductData, accessory.context.deviceId) + .setCharacteristic(this.hap.Characteristic.SerialNumber, accessory.context.deviceId); + } + + async getDeviceLogSettings(device: irdevice & irDevicesConfig): Promise { + if (this.platform.debugMode) { + this.deviceLogging = this.accessory.context.logging = 'debugMode'; + this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); + } else if (device.logging) { + this.deviceLogging = this.accessory.context.logging = device.logging; + this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); + } else if (this.config.logging) { + this.deviceLogging = this.accessory.context.logging = this.config.logging; + this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); + } else { + this.deviceLogging = this.accessory.context.logging = 'standard'; + this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); + } + } + + async getDeviceConfigSettings(device: irdevice & irDevicesConfig): Promise { + const deviceConfig = {}; + if (device.logging !== 'standard') { + deviceConfig['logging'] = device.logging; + } + if (device.connectionType !== '') { + deviceConfig['connectionType'] = device.connectionType; + } + if (device.external === true) { + deviceConfig['external'] = device.external; + } + if (device.customize === true) { + deviceConfig['customize'] = device.customize; + } + if (device.commandType !== '') { + deviceConfig['commandType'] = device.commandType; + } + if (device.customOn !== '') { + deviceConfig['customOn'] = device.customOn; + } + if (device.customOff !== '') { + deviceConfig['customOff'] = device.customOff; + } + if (device.disablePushOn === true) { + deviceConfig['disablePushOn'] = device.disablePushOn; + } + if (device.disablePushOff === true) { + deviceConfig['disablePushOff'] = device.disablePushOff; + } + if (device.disablePushDetail === true) { + deviceConfig['disablePushDetail'] = device.disablePushDetail; + } + let irairConfig = {}; + if (device.irair) { + irairConfig = device.irair; + } + let irpurConfig = {}; + if (device.irpur) { + irpurConfig = device.irpur; + } + let ircamConfig = {}; + if (device.ircam) { + ircamConfig = device.ircam; + } + let irfanConfig = {}; + if (device.irfan) { + irfanConfig = device.irfan; + } + let irlightConfig = {}; + if (device.irlight) { + irlightConfig = device.irlight; + } + let otherConfig = {}; + if (device.other) { + otherConfig = device.other; + } + let irtvConfig = {}; + if (device.irtv) { + irtvConfig = device.irtv; + } + let irvcConfig = {}; + if (device.irvc) { + irvcConfig = device.irvc; + } + let irwhConfig = {}; + if (device.irwh) { + irwhConfig = device.irwh; + } + const config = Object.assign({}, deviceConfig, irairConfig, irpurConfig, ircamConfig, irfanConfig, irlightConfig, otherConfig, + irtvConfig, irvcConfig, irwhConfig); + if (Object.entries(config).length !== 0) { + this.debugSuccessLog(`${this.device.remoteType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); + } + } + + async getDeviceContext(accessory: PlatformAccessory, device: irdevice & irDevicesConfig): Promise { + accessory.context.name = device.deviceName; + accessory.context.model = device.remoteType; + accessory.context.deviceId = device.deviceId; + accessory.context.remoteType = device.remoteType; + if (device.firmware) { + accessory.context.firmware = device.firmware; + } else if (device.firmware === undefined || accessory.context.firmware === undefined) { + device.firmware = this.platform.version; + accessory.context.firmware = device.firmware; + } else { + accessory.context.firmware = 'Unknown'; + } + + // Firmware Version + let deviceFirmwareVersion: string; + if (device.firmware) { + deviceFirmwareVersion = device.firmware; + this.debugSuccessLog(`${device.remoteType}: ${accessory.displayName} 1 FirmwareRevision: ${device.firmware}`); + } else if (accessory.context.deviceVersion) { + deviceFirmwareVersion = accessory.context.deviceVersion; + this.debugSuccessLog(`${device.remoteType}: ${accessory.displayName} 2 FirmwareRevision: ${accessory.context.deviceVersion}`); + } else { + deviceFirmwareVersion = this.platform.version ?? '0.0.0'; + if (this.platform.version) { + this.debugSuccessLog(`${device.remoteType}: ${accessory.displayName} 3 FirmwareRevision: ${this.platform.version}`); + } else { + this.debugSuccessLog(`${device.remoteType}: ${accessory.displayName} 4 FirmwareRevision: ${deviceFirmwareVersion}`); + } + } + const version = deviceFirmwareVersion.toString(); + this.debugLog(`${this.device.remoteType}: ${accessory.displayName} Firmware Version: ${version?.replace(/^V|-.*$/g, '')}`); + let deviceVersion: string; + if (version?.includes('.') === false) { + const replace = version?.replace(/^V|-.*$/g, ''); + const match = replace?.match(/.{1,1}/g); + const validVersion = match?.join('.'); + deviceVersion = validVersion ?? '0.0.0'; + } else { + deviceVersion = version?.replace(/^V|-.*$/g, '') ?? '0.0.0'; + } + accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.SoftwareRevision, deviceVersion) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceVersion) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(deviceVersion); + accessory.context.deviceVersion = deviceVersion; + this.debugSuccessLog(`${device.remoteType}: ${accessory.displayName} deviceVersion: ${accessory.context.deviceVersion}`); + } + + async disablePushOnChanges(device: irdevice & irDevicesConfig): Promise { + if (device.disablePushOn === undefined) { + this.disablePushOn = false; + } else { + this.disablePushOn = device.disablePushOn; + } + } + + async disablePushOffChanges(device: irdevice & irDevicesConfig): Promise { + if (device.disablePushOff === undefined) { + this.disablePushOff = false; + } else { + this.disablePushOff = device.disablePushOff; + } + } + + async disablePushDetailChanges(device: irdevice & irDevicesConfig): Promise { + if (device.disablePushDetail === undefined) { + this.disablePushDetail = false; + } else { + this.disablePushDetail = device.disablePushDetail; + } + } + + async commandType(): Promise { + let commandType: string; + if (this.device.commandType && this.device.customize) { + commandType = this.device.commandType; + } else if (this.device.customize) { + commandType = 'customize'; + } else { + commandType = 'command'; + } + return commandType; + } + + async commandOn(): Promise { + let command: string; + if (this.device.customize && this.device.customOn) { + command = this.device.customOn; + } else { + command = 'turnOn'; + } + return command; + } + + async commandOff(): Promise { + let command: string; + if (this.device.customize && this.device.customOff) { + command = this.device.customOff; + } else { + command = 'turnOff'; + } + return command; + } + + async statusCode(statusCode: number): Promise { + switch (statusCode) { + case 151: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); + break; + case 152: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); + break; + case 160: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); + break; + case 161: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); + break; + case 171: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + + `Hub: ${this.device.hubDeviceId}`); + break; + case 190: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with` + + ` server, Or command format is invalid, statusCode: ${statusCode}`); + break; + case 100: + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); + break; + case 200: + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); + break; + case 400: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` + + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); + break; + case 401: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` + + `but the request has not been authenticated, statusCode: ${statusCode}`); + break; + case 403: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` + + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); + break; + case 404: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` + + `statusCode: ${statusCode}`); + break; + case 406: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` + + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); + break; + case 415: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` + + `header that is not supported by the server, statusCode: ${statusCode}`); + break; + case 422: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, ` + + `but the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); + break; + case 429: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` + + `requests allowed for a given time window, statusCode: ${statusCode}`); + break; + case 500: + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` + + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); + break; + default: + this.infoLog(`${this.device.remoteType}: ${this.accessory.displayName} Unknown statusCode: ` + + `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`); + } + } + + /** + * Logging for Device + */ + infoLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.info(String(...log)); + } + } + + successLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.platform.log.success(String(...log)); + } + } + + debugSuccessLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging?.includes('debug')) { + this.log.success('[DEBUG]', String(...log)); + } + } + } + + warnLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.warn(String(...log)); + } + } + + debugWarnLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging?.includes('debug')) { + this.log.warn('[DEBUG]', String(...log)); + } + } + } + + errorLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.error(String(...log)); + } + } + + debugErrorLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging?.includes('debug')) { + this.log.error('[DEBUG]', String(...log)); + } + } + } + + debugLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging === 'debug') { + this.log.info('[DEBUG]', String(...log)); + } else { + this.log.debug(String(...log)); + } + } + } + + enablingDeviceLogging(): boolean { + return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + } +} \ No newline at end of file diff --git a/src/irdevice/light.ts b/src/irdevice/light.ts index ebefa21f..fe68da5a 100644 --- a/src/irdevice/light.ts +++ b/src/irdevice/light.ts @@ -1,125 +1,127 @@ -import { CharacteristicValue, PlatformAccessory, Service, API, Logging, HAP } from 'homebridge'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * light.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, irDevicesConfig, irdevice, SwitchBotPlatformConfig } from '../settings.js'; +import { Devices } from '../settings.js'; +import { irdeviceBase } from './irdevice.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { irDevicesConfig, irdevice } from '../settings.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class Light { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class Light extends irdeviceBase { // Services - lightBulbService?: Service; - ProgrammableSwitchServiceOn?: Service; - ProgrammableSwitchServiceOff?: Service; - - // Characteristic Values - On!: CharacteristicValue; - ProgrammableSwitchEventOn?: CharacteristicValue; - ProgrammableSwitchOutputStateOn?: CharacteristicValue; - ProgrammableSwitchEventOff?: CharacteristicValue; - ProgrammableSwitchOutputStateOff?: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - - // Config - deviceLogging!: string; - disablePushOn?: boolean; - disablePushOff?: boolean; + private LightBulb?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private ProgrammableSwitchOn?: { + Name: CharacteristicValue; + Service: Service; + ProgrammableSwitchEvent: CharacteristicValue; + ProgrammableSwitchOutputState: CharacteristicValue; + }; + + private ProgrammableSwitchOff?: { + Name: CharacteristicValue; + Service: Service; + ProgrammableSwitchEvent: CharacteristicValue; + ProgrammableSwitchOutputState: CharacteristicValue; + }; constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: irdevice & irDevicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: irdevice & irDevicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // default placeholders - this.deviceLogs(device); - this.deviceContext(); - this.disablePushOnChanges(device); - this.disablePushOffChanges(device); - this.deviceConfig(device); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, device.remoteType) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); + super(platform, accessory, device); if (!device.irlight?.stateless) { - // get the Light service if it exists, otherwise create a new Light service - // you can create multiple services for each accessory - const lightBulbService = `${accessory.displayName} ${device.remoteType}`; - (this.lightBulbService = accessory.getService(this.hap.Service.Lightbulb) - || accessory.addService(this.hap.Service.Lightbulb)), lightBulbService; - - - this.lightBulbService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.lightBulbService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.lightBulbService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - - // handle on / off events using the On characteristic - this.lightBulbService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); + // Initialize LightBulb Service + accessory.context.LightBulb = accessory.context.LightBulb ?? {}; + this.LightBulb = { + Name: accessory.context.LightBulb.Name ?? `${accessory.displayName} ${device.remoteType}`, + Service: accessory.getService(this.hap.Service.Lightbulb) ?? accessory.addService(this.hap.Service.Lightbulb) as Service, + On: accessory.context.On || false, + }; + accessory.context.LightBulb = this.LightBulb as object; + + this.LightBulb.Service + .setCharacteristic(this.hap.Characteristic.Name, this.LightBulb.Name) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.LightBulb!.On; + }) + .onSet(this.OnSet.bind(this)); } else { - - // create a new Stateful Programmable Switch On service - const ProgrammableSwitchServiceOn = `${accessory.displayName} ${device.remoteType} On`; - (this.ProgrammableSwitchServiceOn = accessory.getService(this.hap.Service.StatefulProgrammableSwitch) - || accessory.addService(this.hap.Service.StatefulProgrammableSwitch)), ProgrammableSwitchServiceOn; - - - this.ProgrammableSwitchServiceOn.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} On`); - if (!this.ProgrammableSwitchServiceOn.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.ProgrammableSwitchServiceOn.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} On`); - } - - this.ProgrammableSwitchServiceOn.getCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent).setProps({ - validValueRanges: [0, 0], - minValue: 0, - maxValue: 0, - validValues: [0], - }) + // Initialize ProgrammableSwitchOn Service + accessory.context.ProgrammableSwitchOn = accessory.context.ProgrammableSwitchOn ?? {}; + this.ProgrammableSwitchOn = { + Name: accessory.context.ProgrammableSwitchOn.Name ?? `${accessory.displayName} ${device.remoteType} On`, + Service: accessory.getService(this.hap.Service.StatefulProgrammableSwitch) + ?? accessory.addService(this.hap.Service.StatefulProgrammableSwitch) as Service, + ProgrammableSwitchEvent: accessory.context.ProgrammableSwitchEvent ?? this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, + ProgrammableSwitchOutputState: accessory.context.ProgrammableSwitchOutputState ?? 0, + }; + accessory.context.ProgrammableSwitchOn = this.ProgrammableSwitchOn as object; + + // Initialize ProgrammableSwitchOn Characteristics + this.ProgrammableSwitchOn?.Service + .setCharacteristic(this.hap.Characteristic.Name, this.ProgrammableSwitchOn.Name) + .getCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent).setProps({ + validValueRanges: [0, 0], + minValue: 0, + maxValue: 0, + validValues: [0], + }) .onGet(() => { - return this.ProgrammableSwitchEventOn!; + return this.ProgrammableSwitchOn!.ProgrammableSwitchEvent; }); - this.ProgrammableSwitchServiceOn.getCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState) + this.ProgrammableSwitchOn?.Service + .getCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState) + .onGet(() => { + return this.ProgrammableSwitchOn!.ProgrammableSwitchOutputState; + }) .onSet(this.ProgrammableSwitchOutputStateSetOn.bind(this)); - - - // create a new Stateful Programmable Switch Off service - const ProgrammableSwitchServiceOff = `${accessory.displayName} ${device.remoteType} Off`; - (this.ProgrammableSwitchServiceOff = accessory.getService(this.hap.Service.StatefulProgrammableSwitch) - || accessory.addService(this.hap.Service.StatefulProgrammableSwitch)), ProgrammableSwitchServiceOff; - - - this.ProgrammableSwitchServiceOff.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Off`); - if (!this.ProgrammableSwitchServiceOff.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.ProgrammableSwitchServiceOff.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Off`); - } - - this.ProgrammableSwitchServiceOff.getCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent).setProps({ - validValueRanges: [0, 0], - minValue: 0, - maxValue: 0, - validValues: [0], - }) + // Initialize ProgrammableSwitchOff Service + accessory.context.ProgrammableSwitchOff = accessory.context.ProgrammableSwitchOff ?? {}; + this.ProgrammableSwitchOff = { + Name: accessory.context.ProgrammableSwitchOff.Name ?? `${accessory.displayName} ${device.remoteType} Off`, + Service: accessory.getService(this.hap.Service.StatefulProgrammableSwitch) + ?? accessory.addService(this.hap.Service.StatefulProgrammableSwitch) as Service, + ProgrammableSwitchEvent: accessory.context.ProgrammableSwitchEvent ?? this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, + ProgrammableSwitchOutputState: accessory.context.ProgrammableSwitchOutputState ?? 0, + }; + accessory.context.ProgrammableSwitchOff = this.ProgrammableSwitchOff as object; + + // Initialize ProgrammableSwitchOff Characteristics + this.ProgrammableSwitchOff?.Service + .setCharacteristic(this.hap.Characteristic.Name, this.ProgrammableSwitchOff.Name) + .getCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent).setProps({ + validValueRanges: [0, 0], + minValue: 0, + maxValue: 0, + validValues: [0], + }) .onGet(() => { - return this.ProgrammableSwitchEventOff!; + return this.ProgrammableSwitchOff!.ProgrammableSwitchEvent; }); - this.ProgrammableSwitchServiceOff.getCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState) + this.ProgrammableSwitchOff?.Service + .getCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState) + .onGet(() => { + return this.ProgrammableSwitchOff!.ProgrammableSwitchOutputState; + }) .onSet(this.ProgrammableSwitchOutputStateSetOff.bind(this)); } @@ -128,11 +130,13 @@ export class Light { async OnSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${value}`); - this.On = value; - if (this.On) { - await this.pushLightOnChanges(); + this.LightBulb!.On = value; + if (this.LightBulb?.On) { + const On = true; + await this.pushLightOnChanges(On); } else { - await this.pushLightOffChanges(); + const On = false; + await this.pushLightOffChanges(On); } /** * pushLightOnChanges and pushLightOffChanges above assume they are measuring the state of the accessory BEFORE @@ -143,10 +147,10 @@ export class Light { async ProgrammableSwitchOutputStateSetOn(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${value}`); - this.ProgrammableSwitchOutputStateOn = value; - if (this.ProgrammableSwitchOutputStateOn === 1) { - this.On = true; - await this.pushLightOnChanges(); + this.ProgrammableSwitchOn!.ProgrammableSwitchOutputState = value; + if (this.ProgrammableSwitchOn?.ProgrammableSwitchOutputState === 1) { + const On = true; + await this.pushLightOnChanges(On); } /** * pushLightOnChanges and pushLightOffChanges above assume they are measuring the state of the accessory BEFORE @@ -157,10 +161,10 @@ export class Light { async ProgrammableSwitchOutputStateSetOff(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${value}`); - this.ProgrammableSwitchOutputStateOff = value; - if (this.ProgrammableSwitchOutputStateOff === 1) { - this.On = false; - await this.pushLightOffChanges(); + this.ProgrammableSwitchOff!.ProgrammableSwitchOutputState = value; + if (this.ProgrammableSwitchOff?.ProgrammableSwitchOutputState === 1) { + const On = false; + await this.pushLightOffChanges(On); } /** * pushLightOnChanges and pushLightOffChanges above assume they are measuring the state of the accessory BEFORE @@ -180,11 +184,9 @@ export class Light { * Light - "command" "channelAdd" "default" = next channel * Light - "command" "channelSub" "default" = previous channel */ - async pushLightOnChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushLightOnChanges On: ${this.On},` + ` disablePushOn: ${this.disablePushOn}`, - ); - if (this.On && !this.disablePushOn) { + async pushLightOnChanges(On: boolean): Promise { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushLightOnChanges On: ${On}, disablePushOn: ${this.disablePushOn}`); + if (On === true && this.disablePushOn === false) { const commandType: string = await this.commandType(); const command: string = await this.commandOn(); const bodyChange = JSON.stringify({ @@ -192,15 +194,13 @@ export class Light { parameter: 'default', commandType: commandType, }); - await this.pushChanges(bodyChange); + await this.pushChanges(bodyChange, On); } } - async pushLightOffChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushLightOffChanges On: ${this.On},` + ` disablePushOff: ${this.disablePushOff}`, - ); - if (!this.On && !this.disablePushOff) { + async pushLightOffChanges(On: boolean): Promise { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushLightOffChanges On: ${On}, disablePushOff: ${this.disablePushOff}`); + if (On === false && this.disablePushOff === false) { const commandType: string = await this.commandType(); const command: string = await this.commandOff(); const bodyChange = JSON.stringify({ @@ -208,29 +208,11 @@ export class Light { parameter: 'default', commandType: commandType, }); - await this.pushChanges(bodyChange); + await this.pushChanges(bodyChange, On); } } - /*async pushLightBrightnessUpChanges(): Promise { - const bodyChange = JSON.stringify({ - command: 'brightnessUp', - parameter: 'default', - commandType: 'command', - }); - await this.pushChanges(bodyChange); - } - - async pushLightBrightnessDownChanges(): Promise { - const bodyChange = JSON.stringify({ - command: 'brightnessDown', - parameter: 'default', - commandType: 'command', - }); - await this.pushChanges(bodyChange); - }*/ - - async pushChanges(bodyChange: any): Promise { + async pushChanges(bodyChange: any, On: boolean): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushChanges`); if (this.device.connectionType === 'OpenAPI') { this.infoLog(`${this.device.remoteType}: ${this.accessory.displayName} Sending request to SwitchBot API, body: ${bodyChange},`); @@ -245,9 +227,11 @@ export class Light { this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); - this.accessory.context.On = this.On; + this.successLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); + this.accessory.context.On = On; this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -255,298 +239,59 @@ export class Light { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.warnLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`, - ); + this.warnLog(`${this.device.remoteType}: ${this.accessory.displayName} Connection Type: ` + + `${this.device.connectionType}, commands will not be sent to OpenAPI`); } } async updateHomeKitCharacteristics(): Promise { - if (this.device.irlight?.stateless) { + if (!this.device.irlight?.stateless) { // On - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.LightBulb?.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.LightBulb?.On}`); } else { - this.accessory.context.On = this.On; - this.lightBulbService?.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + this.accessory.context.On = this.LightBulb.On; + this.LightBulb?.Service.updateCharacteristic(this.hap.Characteristic.On, this.LightBulb.On); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.LightBulb.On}`); } } else { // On Stateful Programmable Switch - if (this.ProgrammableSwitchOutputStateOn === undefined) { + if (this.ProgrammableSwitchOn?.ProgrammableSwitchOutputState === undefined) { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName}` - + ` ProgrammableSwitchOutputStateOn: ${this.ProgrammableSwitchOutputStateOn}`); + + ` ProgrammableSwitchOutputStateOn: ${this.ProgrammableSwitchOn?.ProgrammableSwitchOutputState}`); } else { - this.accessory.context.ProgrammableSwitchOutputStateOn = this.ProgrammableSwitchOutputStateOn; - this.ProgrammableSwitchServiceOn?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, - this.ProgrammableSwitchOutputStateOn); + this.accessory.context.ProgrammableSwitchOutputStateOn = this.ProgrammableSwitchOn.ProgrammableSwitchOutputState; + this.ProgrammableSwitchOn?.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, + this.ProgrammableSwitchOn.ProgrammableSwitchOutputState); this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` - + ` ProgrammableSwitchOutputStateOn: ${this.ProgrammableSwitchOutputStateOn}`); + + ` ProgrammableSwitchOutputStateOn: ${this.ProgrammableSwitchOn.ProgrammableSwitchOutputState}`); } // Off Stateful Programmable Switch - if (this.ProgrammableSwitchOutputStateOff === undefined) { + if (this.ProgrammableSwitchOff?.ProgrammableSwitchOutputState === undefined) { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName}` - + ` ProgrammableSwitchOutputStateOff: ${this.ProgrammableSwitchOutputStateOff}`); + + ` ProgrammableSwitchOutputStateOff: ${this.ProgrammableSwitchOff?.ProgrammableSwitchOutputState}`); } else { - this.accessory.context.ProgrammableSwitchOutputStateOff = this.ProgrammableSwitchOutputStateOff; - this.ProgrammableSwitchServiceOff?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, - this.ProgrammableSwitchOutputStateOff); + this.accessory.context.ProgrammableSwitchOutputStateOff = this.ProgrammableSwitchOff.ProgrammableSwitchOutputState; + this.ProgrammableSwitchOff.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, + this.ProgrammableSwitchOff.ProgrammableSwitchOutputState); this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` - + ` ProgrammableSwitchOutputStateOff: ${this.ProgrammableSwitchOutputStateOff}`); + + ` ProgrammableSwitchOutputStateOff: ${this.ProgrammableSwitchOff?.ProgrammableSwitchOutputState}`); } } } - async disablePushOnChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOn === undefined) { - this.disablePushOn = false; - } else { - this.disablePushOn = device.disablePushOn; - } - } - - async disablePushOffChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOff === undefined) { - this.disablePushOff = false; - } else { - this.disablePushOff = device.disablePushOff; - } - } - - async commandType(): Promise { - let commandType: string; - if (this.device.customize) { - commandType = 'customize'; - } else { - commandType = 'command'; - } - return commandType; - } - - async commandOn(): Promise { - let command: string; - if (this.device.customize && this.device.customOn) { - command = this.device.customOn; - } else { - command = 'turnOn'; - } - return command; - } - - async commandOff(): Promise { - let command: string; - if (this.device.customize && this.device.customOff) { - command = this.device.customOff; - } else { - command = 'turnOff'; - } - return command; - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - break; - case 171: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - break; - case 190: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, but ` - + `the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.remoteType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async apiError(e: any): Promise { - if (this.device.irlight?.stateless) { - this.lightBulbService?.updateCharacteristic(this.hap.Characteristic.On, e); - } else { - this.ProgrammableSwitchServiceOn?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, e); - this.ProgrammableSwitchServiceOn?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, e); - this.ProgrammableSwitchServiceOff?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, e); - this.ProgrammableSwitchServiceOff?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, e); - } - } - - async deviceContext() { - if (this.On === undefined) { - this.On = false; - } else { - this.On = this.accessory.context.On; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async deviceConfig(device: irdevice & irDevicesConfig): Promise { - let config = {}; - if (device.irlight) { - config = device.irlight; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.customOn !== undefined) { - config['customOn'] = device.customOn; - } - if (device.customOff !== undefined) { - config['customOff'] = device.customOff; - } - if (device.customize !== undefined) { - config['customize'] = device.customize; - } - if (device.disablePushOn !== undefined) { - config['disablePushOn'] = device.disablePushOn; - } - if (device.disablePushOff !== undefined) { - config['disablePushOff'] = device.disablePushOff; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: irdevice & irDevicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); + if (!this.device.irlight?.stateless) { + this.LightBulb?.Service.updateCharacteristic(this.hap.Characteristic.On, e); } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } + this.ProgrammableSwitchOn?.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, e); + this.ProgrammableSwitchOn?.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, e); + this.ProgrammableSwitchOff?.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, e); + this.ProgrammableSwitchOff?.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, e); } } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; - } } diff --git a/src/irdevice/other.ts b/src/irdevice/other.ts index 467f95be..6bb3064d 100644 --- a/src/irdevice/other.ts +++ b/src/irdevice/other.ts @@ -1,67 +1,116 @@ -import { CharacteristicValue, PlatformAccessory, Service, API, Logging, HAP } from 'homebridge'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * other.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, irDevicesConfig, irdevice, SwitchBotPlatformConfig } from '../settings.js'; +import { Devices } from '../settings.js'; +import { irdeviceBase } from './irdevice.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { irDevicesConfig, irdevice } from '../settings.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class Others { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class Others extends irdeviceBase { // Services - fanService?: Service; - doorService?: Service; - lockService?: Service; - faucetService?: Service; - windowService?: Service; - switchService?: Service; - outletService?: Service; - garageDoorService?: Service; - windowCoveringService?: Service; - statefulProgrammableSwitchService?: Service; - - // Characteristic Values - On?: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; + private Switch?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private GarageDoor?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private Door?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private Window?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private WindowCovering?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private LockMechanism?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private Faucet?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private Fan?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private StatefulProgrammableSwitch?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; + + private Outlet?: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; // Config - deviceLogging!: string; - disablePushOn?: boolean; otherDeviceType?: string; - disablePushOff?: boolean; constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: irdevice & irDevicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: irdevice & irDevicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // default placeholders - this.deviceLogs(device); - this.deviceType(device); - this.deviceContext(); - this.disablePushOnChanges(device); - this.disablePushOffChanges(device); - this.deviceConfig(device); + super(platform, accessory, device); - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, device.remoteType) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); + // default placeholders + this.getOtherConfigSettings(device); // deviceType if (this.otherDeviceType === 'switch') { + // Initialize Switch Service + accessory.context.Switch = accessory.context.Switch ?? {}; + this.Switch = { + Name: accessory.context.Switch.Name ?? `${accessory.displayName} Switch`, + Service: accessory.getService(this.hap.Service.Switch) ?? accessory.addService(this.hap.Service.Switch) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Switch = this.Switch as object; + this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Switch`); + + this.Switch.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.Switch!.On; + }) + .onSet(this.OnSet.bind(this)); + + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -71,19 +120,27 @@ export class Others { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.otherDeviceType === 'garagedoor') { + // Initialize Garage Door Service + accessory.context.GarageDoor = accessory.context.GarageDoor ?? {}; + this.GarageDoor = { + Name: accessory.context.GarageDoor.Name ?? `${accessory.displayName} Garage Door`, + Service: accessory.getService(this.hap.Service.GarageDoorOpener) ?? accessory.addService(this.hap.Service.GarageDoorOpener) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.GarageDoor = this.GarageDoor as object; + this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Garage Door Opener`); - // Add switchService - const switchService = `${accessory.displayName} Switch`; - (this.switchService = accessory.getService(this.hap.Service.Switch) - || accessory.addService(this.hap.Service.Switch)), switchService; - this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Switch`); + this.GarageDoor.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .setCharacteristic(this.hap.Characteristic.ObstructionDetected, false) + .getCharacteristic(this.hap.Characteristic.TargetDoorState) + .onGet(() => { + return this.GarageDoor!.On; + }) + .onSet(this.OnSet.bind(this)); - this.switchService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.switchService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.switchService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.switchService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); - } else if (this.otherDeviceType === 'garagedoor') { + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -93,20 +150,33 @@ export class Others { this.removeWindowService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.otherDeviceType === 'door') { + // Initialize Door Service + accessory.context.Door = accessory.context.Door ?? {}; + this.Door = { + Name: accessory.context.Door.Name ?? `${accessory.displayName} Door`, + Service: accessory.getService(this.hap.Service.Door) ?? accessory.addService(this.hap.Service.Door) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Door = this.Door as object; + this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Door`); - // Add garageDoorService - const garageDoorService = `${accessory.displayName} Garage Door Opener`; - (this.garageDoorService = accessory.getService(this.hap.Service.GarageDoorOpener) - || accessory.addService(this.hap.Service.GarageDoorOpener)), garageDoorService; - this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Garage Door Opener`); + this.Door!.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED) + .getCharacteristic(this.hap.Characteristic.TargetPosition) + .setProps({ + validValues: [0, 100], + minValue: 0, + maxValue: 100, + minStep: 100, + }) + .onGet(() => { + return this.GarageDoor!.On; + }) + .onSet(this.OnSet.bind(this)); - this.garageDoorService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.garageDoorService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.garageDoorService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.garageDoorService.getCharacteristic(this.hap.Characteristic.TargetDoorState).onSet(this.OnSet.bind(this)); - this.garageDoorService.setCharacteristic(this.hap.Characteristic.ObstructionDetected, false); - } else if (this.otherDeviceType === 'door') { + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeOutletService(accessory); @@ -116,18 +186,19 @@ export class Others { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.otherDeviceType === 'window') { + // Initialize Window Service + accessory.context.Window = accessory.context.Window ?? {}; + this.Window = { + Name: accessory.context.Window.Name ?? `${accessory.displayName} Window`, + Service: accessory.getService(this.hap.Service.Window) ?? accessory.addService(this.hap.Service.Window) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Window = this.Window as object; + this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Window`); - // Add doorService - const doorService = `${accessory.displayName} Door`; - (this.doorService = accessory.getService(this.hap.Service.Door) - || accessory.addService(this.hap.Service.Door)), doorService; - this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Door`); - - this.doorService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.doorService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.doorService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.doorService + this.Window!.Service.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED) .getCharacteristic(this.hap.Characteristic.TargetPosition) .setProps({ validValues: [0, 100], @@ -135,9 +206,12 @@ export class Others { maxValue: 100, minStep: 100, }) + .onGet(() => { + return this.GarageDoor!.On; + }) .onSet(this.OnSet.bind(this)); - this.doorService.setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - } else if (this.otherDeviceType === 'window') { + + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -147,18 +221,20 @@ export class Others { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.otherDeviceType === 'windowcovering') { + // Initialize WindowCovering Service + accessory.context.WindowCovering = accessory.context.WindowCovering ?? {}; + this.WindowCovering = { + Name: accessory.context.WindowCovering.Name ?? `${accessory.displayName} Window Covering`, + Service: accessory.getService(this.hap.Service.WindowCovering) ?? accessory.addService(this.hap.Service.WindowCovering) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.WindowCovering = this.WindowCovering as object; + this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Window Covering`); - // Add windowService - const windowService = `${accessory.displayName} Window`; - (this.windowService = accessory.getService(this.hap.Service.Window) - || accessory.addService(this.hap.Service.Window)), windowService; - this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Window`); - - this.windowService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.windowService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.windowService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.windowService + this.WindowCovering.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED) .getCharacteristic(this.hap.Characteristic.TargetPosition) .setProps({ validValues: [0, 100], @@ -166,9 +242,12 @@ export class Others { maxValue: 100, minStep: 100, }) + .onGet(() => { + return this.WindowCovering!.On; + }) .onSet(this.OnSet.bind(this)); - this.windowService.setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - } else if (this.otherDeviceType === 'windowcovering') { + + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -178,28 +257,26 @@ export class Others { this.removeWindowService(accessory); this.removeGarageDoorService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.otherDeviceType === 'lock') { + // Initialize LockMechanism Service + accessory.context.LockMechanism = accessory.context.LockMechanism ?? {}; + this.LockMechanism = { + Name: accessory.context.LockMechanism.Name ?? `${accessory.displayName} Lock`, + Service: accessory.getService(this.hap.Service.LockMechanism) ?? accessory.addService(this.hap.Service.LockMechanism) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.LockMechanism = this.LockMechanism as object; + this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Lock`); - // Add windowCoveringService - const windowCoveringService = `${accessory.displayName} Window Covering`; - (this.windowCoveringService = accessory.getService(this.hap.Service.WindowCovering) - || accessory.addService(this.hap.Service.WindowCovering)), windowCoveringService; - this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Window Covering`); - - this.windowCoveringService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.windowCoveringService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.windowCoveringService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.windowCoveringService - .getCharacteristic(this.hap.Characteristic.TargetPosition) - .setProps({ - validValues: [0, 100], - minValue: 0, - maxValue: 100, - minStep: 100, + this.LockMechanism.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .getCharacteristic(this.hap.Characteristic.LockTargetState) + .onGet(() => { + return this.LockMechanism!.On; }) .onSet(this.OnSet.bind(this)); - this.windowCoveringService.setCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - } else if (this.otherDeviceType === 'lock') { + + // Remove other services this.removeFanService(accessory); this.removeDoorService(accessory); this.removeOutletService(accessory); @@ -209,19 +286,26 @@ export class Others { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.otherDeviceType === 'faucet') { + // Initialize Faucet Service + accessory.context.Faucet = accessory.context.Faucet ?? {}; + this.Faucet = { + Name: accessory.context.Faucet.Name ?? `${accessory.displayName} Faucet`, + Service: accessory.getService(this.hap.Service.Faucet) ?? accessory.addService(this.hap.Service.Faucet) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Faucet = this.Faucet as object; + this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Faucet`); - // Add lockService - const lockService = `${accessory.displayName} Lock`; - (this.lockService = accessory.getService(this.hap.Service.LockMechanism) - || accessory.addService(this.hap.Service.LockMechanism)), lockService; - this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Lock`); + this.Faucet.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .getCharacteristic(this.hap.Characteristic.Active) + .onGet(() => { + return this.Faucet!.On; + }) + .onSet(this.OnSet.bind(this)); - this.lockService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.lockService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.lockService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.lockService.getCharacteristic(this.hap.Characteristic.LockTargetState).onSet(this.OnSet.bind(this)); - } else if (this.otherDeviceType === 'faucet') { + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -231,19 +315,26 @@ export class Others { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.otherDeviceType === 'fan') { + // Initialize Fan Service + accessory.context.Fan = accessory.context.Fan ?? {}; + this.Fan = { + Name: accessory.context.Fan.Name ?? `${accessory.displayName} Fan`, + Service: accessory.getService(this.hap.Service.Fanv2) ?? accessory.addService(this.hap.Service.Fanv2) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Fan = this.Fan as object; + this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Fan`); - // Add faucetService - const faucetService = `${accessory.displayName} Faucet`; - (this.faucetService = accessory.getService(this.hap.Service.Faucet) - || accessory.addService(this.hap.Service.Faucet)), faucetService; - this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Faucet`); + this.Fan.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.Fan!.On; + }) + .onSet(this.OnSet.bind(this)); - this.faucetService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.faucetService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.faucetService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.faucetService.getCharacteristic(this.hap.Characteristic.Active).onSet(this.OnSet.bind(this)); - } else if (this.otherDeviceType === 'fan') { + // Remove other services this.removeLockService(accessory); this.removeDoorService(accessory); this.removeFaucetService(accessory); @@ -253,19 +344,27 @@ export class Others { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); + } else if (this.otherDeviceType === 'stateful') { + // Initialize StatefulProgrammableSwitch Service + accessory.context.StatefulProgrammableSwitch = accessory.context.StatefulProgrammableSwitch ?? {}; + this.StatefulProgrammableSwitch = { + Name: accessory.context.StatefulProgrammableSwitch.Name ?? `${accessory.displayName} Stateful Programmable Switch`, + Service: accessory.getService(this.hap.Service.StatefulProgrammableSwitch) + ?? accessory.addService(this.hap.Service.StatefulProgrammableSwitch) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.StatefulProgrammableSwitch = this.StatefulProgrammableSwitch as object; + this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Stateful Programmable Switch`); - // Add fanService - const fanService = `${accessory.displayName} Fan`; - (this.fanService = accessory.getService(this.hap.Service.Fan) - || accessory.addService(this.hap.Service.Fan)), fanService; - this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Fan`); + this.StatefulProgrammableSwitch.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .getCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState) + .onGet(() => { + return this.StatefulProgrammableSwitch!.On; + }) + .onSet(this.OnSet.bind(this)); - this.fanService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.fanService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.fanService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.fanService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); - } else if (this.otherDeviceType === 'stateful') { + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -275,21 +374,26 @@ export class Others { this.removeWindowService(accessory); this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); + } else { + // Initialize Outlet Service + accessory.context.Outlet = accessory.context.Outlet ?? {}; + this.Outlet = { + Name: accessory.context.Outlet.Name ?? `${accessory.displayName} Outlet`, + Service: accessory.getService(this.hap.Service.Outlet) ?? accessory.addService(this.hap.Service.Outlet) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Outlet = this.Outlet as object; + this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Outlet`); - // Add statefulProgrammableSwitchService - const statefulProgrammableSwitchService = `${accessory.displayName} Stateful Programmable Switch`; - (this.statefulProgrammableSwitchService = accessory.getService(this.hap.Service.StatefulProgrammableSwitch) - || accessory.addService(this.hap.Service.StatefulProgrammableSwitch)), statefulProgrammableSwitchService; - this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Stateful Programmable Switch`); - - this.statefulProgrammableSwitchService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.statefulProgrammableSwitchService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.statefulProgrammableSwitchService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.statefulProgrammableSwitchService - .getCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState) + this.Outlet.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.Outlet!.On; + }) .onSet(this.OnSet.bind(this)); - } else { + + // Remove other services this.removeFanService(accessory); this.removeLockService(accessory); this.removeDoorService(accessory); @@ -299,18 +403,6 @@ export class Others { this.removeGarageDoorService(accessory); this.removeWindowCoveringService(accessory); this.removeStatefulProgrammableSwitchService(accessory); - - // Add outletService - const outletService = `${accessory.displayName} Outlet`; - (this.outletService = accessory.getService(this.hap.Service.Outlet) - || accessory.addService(this.hap.Service.Outlet)), outletService; - this.debugWarnLog(`${this.device.remoteType}: ${accessory.displayName} Displaying as Outlet`); - - this.outletService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.outletService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.outletService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - this.outletService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); } } @@ -318,55 +410,75 @@ export class Others { * Handle requests to set the "On" characteristic */ async OnSet(value: CharacteristicValue): Promise { + let On: boolean; if (this.otherDeviceType === 'garagedoor') { this.infoLog(`${this.device.remoteType}: ${this.accessory.displayName} Set TargetDoorState: ${value}`); if (value === this.hap.Characteristic.TargetDoorState.CLOSED) { - this.On = false; + this.GarageDoor!.On = false; + } else { + this.GarageDoor!.On = true; + } + On = this.GarageDoor!.On; + } else if (this.otherDeviceType === 'door') { + this.infoLog(`${this.device.remoteType}: ${this.accessory.displayName} Set TargetPosition: ${value}`); + if (value === 0) { + this.Door!.On = false; } else { - this.On = true; + this.Door!.On = true; } - } else if ( - this.otherDeviceType === 'door' || - this.otherDeviceType === 'window' || - this.otherDeviceType === 'windowcovering' - ) { + On = this.Door!.On; + } else if (this.otherDeviceType === 'window') { + this.infoLog(`${this.device.remoteType}: ${this.accessory.displayName} Set TargetPosition: ${value}`); + if (value === 0) { + this.Window!.On = false; + } else { + this.Window!.On = true; + } + On = this.Window!.On; + } else if (this.otherDeviceType === 'windowcovering') { this.infoLog(`${this.device.remoteType}: ${this.accessory.displayName} Set TargetPosition: ${value}`); if (value === 0) { - this.On = false; + this.WindowCovering!.On = false; } else { - this.On = true; + this.WindowCovering!.On = true; } + On = this.WindowCovering!.On; } else if (this.otherDeviceType === 'lock') { this.infoLog(`${this.device.remoteType}: ${this.accessory.displayName} Set LockTargetState: ${value}`); if (value === this.hap.Characteristic.LockTargetState.SECURED) { - this.On = false; + this.LockMechanism!.On = false; } else { - this.On = true; + this.LockMechanism!.On = true; } + On = this.LockMechanism!.On; } else if (this.otherDeviceType === 'faucet') { this.infoLog(`${this.device.remoteType}: ${this.accessory.displayName} Set Active: ${value}`); if (value === this.hap.Characteristic.Active.INACTIVE) { - this.On = false; + this.Faucet!.On = false; } else { - this.On = true; + this.Faucet!.On = true; } + On = this.Faucet!.On; } else if (this.otherDeviceType === 'stateful') { this.infoLog(`${this.device.remoteType}: ${this.accessory.displayName} Set ProgrammableSwitchOutputState: ${value}`); if (value === 0) { - this.On = false; + this.StatefulProgrammableSwitch!.On = false; } else { - this.On = true; + this.StatefulProgrammableSwitch!.On = true; } + + On = this.StatefulProgrammableSwitch!.On; } else { this.infoLog(`${this.device.remoteType}: ${this.accessory.displayName} Set On: ${value}`); - this.On = value; + this.Outlet!.On = value; + On = this.Outlet!.On ? true : false; } //pushChanges - if (this.On) { - await this.pushOnChanges(); + if (On === true) { + await this.pushOnChanges(On); } else { - await this.pushOffChanges(); + await this.pushOffChanges(On); } } @@ -380,13 +492,11 @@ export class Others { * Other - "command" "channelAdd" "default" = next channel * Other - "command" "channelSub" "default" = previous channel */ - async pushOnChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushOnChanges On: ${this.On},` + - ` disablePushOn: ${this.disablePushOn}, customize: ${this.device.customize}, customOn: ${this.device.customOn}`, - ); + async pushOnChanges(On: boolean): Promise { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushOnChanges On: ${On},` + + ` disablePushOn: ${this.disablePushOn}, customize: ${this.device.customize}, customOn: ${this.device.customOn}`); if (this.device.customize) { - if (this.On && !this.disablePushOn) { + if (On === true && !this.disablePushOn) { const commandType: string = await this.commandType(); const command: string = await this.commandOn(); const bodyChange = JSON.stringify({ @@ -401,13 +511,11 @@ export class Others { } } - async pushOffChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushOffChanges On: ${this.On},` + - ` disablePushOff: ${this.disablePushOff}, customize: ${this.device.customize}, customOff: ${this.device.customOff}`, - ); + async pushOffChanges(On: boolean): Promise { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushOffChanges On: ${On},` + + ` disablePushOff: ${this.disablePushOff}, customize: ${this.device.customize}, customOff: ${this.device.customOff}`); if (this.device.customize) { - if (!this.On && !this.disablePushOff) { + if (On === false && !this.disablePushOff) { const commandType: string = await this.commandType(); const command: string = await this.commandOff(); const bodyChange = JSON.stringify({ @@ -437,8 +545,10 @@ export class Others { this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -446,456 +556,318 @@ export class Others { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.warnLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`, - ); + this.warnLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`); } } async updateHomeKitCharacteristics(): Promise { if (this.otherDeviceType === 'garagedoor') { - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.GarageDoor!.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.GarageDoor!.On}`); } else { - if (this.On) { - this.garageDoorService?.updateCharacteristic( - this.hap.Characteristic.TargetDoorState, - this.hap.Characteristic.TargetDoorState.OPEN, - ); - this.garageDoorService?.updateCharacteristic( - this.hap.Characteristic.CurrentDoorState, - this.hap.Characteristic.CurrentDoorState.OPEN, - ); - this.debugLog( - `${this.device.remoteType}: ` + `${this.accessory.displayName} updateCharacteristic TargetDoorState: Open, CurrentDoorState: Open`, - ); + if (this.GarageDoor!.On) { + this.GarageDoor!.Service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.hap.Characteristic.TargetDoorState.OPEN); + this.GarageDoor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.OPEN); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetDoorState: Open, CurrentDoorState: Open (${this.GarageDoor!.On})`); } else { - this.garageDoorService?.updateCharacteristic( - this.hap.Characteristic.TargetDoorState, - this.hap.Characteristic.TargetDoorState.CLOSED, - ); - this.garageDoorService?.updateCharacteristic( - this.hap.Characteristic.CurrentDoorState, - this.hap.Characteristic.CurrentDoorState.CLOSED, - ); - this.debugLog( - `${this.device.remoteType}: ` + `${this.accessory.displayName} updateCharacteristic TargetDoorState: Open, CurrentDoorState: Open`, - ); + this.GarageDoor!.Service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.hap.Characteristic.TargetDoorState.CLOSED); + this.GarageDoor!.Service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.CLOSED); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetDoorState: Closed, CurrentDoorState: Closed (${this.GarageDoor!.On})`); } } - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Garage Door On: ${this.On}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Garage Door On: ${this.GarageDoor!.On}`); } else if (this.otherDeviceType === 'door') { - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Door!.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.Door!.On}`); } else { - if (this.On) { - this.doorService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); - this.doorService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); - this.doorService?.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 100, CurrentPosition: 100`); + if (this.Door!.On) { + this.Door!.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); + this.Door!.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); + this.Door!.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetPosition: 100, CurrentPosition: 100 (${this.Door!.On})`); } else { - this.doorService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); - this.doorService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); - this.doorService?.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 0, CurrentPosition: 0`); + this.Door!.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); + this.Door!.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); + this.Door!.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetPosition: 0, CurrentPosition: 0 (${this.Door!.On})`); } } - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Door On: ${this.On}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Door On: ${this.Door!.On}`); } else if (this.otherDeviceType === 'window') { - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Window!.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.Window!.On}`); } else { - if (this.On) { - this.windowService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); - this.windowService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); - this.windowService?.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 100, CurrentPosition: 100`); + if (this.Window!.On) { + this.Window!.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); + this.Window!.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); + this.Window!.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetPosition: 100, CurrentPosition: 100 (${this.Window!.On})`); } else { - this.windowService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); - this.windowService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); - this.windowService?.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 0, CurrentPosition: 0`); + this.Window!.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); + this.Window!.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); + this.Window!.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetPosition: 0, CurrentPosition: 0 (${this.Window!.On})`); } } - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Window On: ${this.On}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Window On: ${this.Window!.On}`); } else if (this.otherDeviceType === 'windowcovering') { - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.WindowCovering!.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.WindowCovering!.On}`); } else { - if (this.On) { - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); - this.windowCoveringService?.updateCharacteristic( - this.hap.Characteristic.PositionState, - this.hap.Characteristic.PositionState.STOPPED, - ); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 100, CurrentPosition: 100`); + if (this.WindowCovering!.On) { + this.WindowCovering!.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 100); + this.WindowCovering!.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 100); + this.WindowCovering!.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetPosition: 100, CurrentPosition: 100 (${this.WindowCovering!.On})`); } else { - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); - this.windowCoveringService?.updateCharacteristic( - this.hap.Characteristic.PositionState, - this.hap.Characteristic.PositionState.STOPPED, - ); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic TargetPosition: 0, CurrentPosition: 0`); + this.WindowCovering!.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, 0); + this.WindowCovering!.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, 0); + this.WindowCovering!.Service.updateCharacteristic(this.hap.Characteristic.PositionState, this.hap.Characteristic.PositionState.STOPPED); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` TargetPosition: 0, CurrentPosition: 0 (${this.WindowCovering!.On})`); } } - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Window Covering On: ${this.On}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Window Covering On: ${this.WindowCovering!.On}`); } else if (this.otherDeviceType === 'lock') { - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.LockMechanism?.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.LockMechanism?.On}`); } else { - if (this.On) { - this.lockService?.updateCharacteristic( - this.hap.Characteristic.LockTargetState, - this.hap.Characteristic.LockTargetState.UNSECURED, - ); - this.lockService?.updateCharacteristic( - this.hap.Characteristic.LockCurrentState, - this.hap.Characteristic.LockCurrentState.UNSECURED, - ); - this.debugLog( - `${this.device.remoteType}: ` + - `${this.accessory.displayName} updateCharacteristic LockTargetState: UNSECURED, LockCurrentState: UNSECURED`, - ); + if (this.LockMechanism.On) { + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockTargetState, + this.hap.Characteristic.LockTargetState.UNSECURED); + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, + this.hap.Characteristic.LockCurrentState.UNSECURED); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` LockTargetState: UNSECURED, LockCurrentState: UNSECURE (${this.LockMechanism.On})`); } else { - this.lockService?.updateCharacteristic(this.hap.Characteristic.LockTargetState, this.hap.Characteristic.LockTargetState.SECURED); - this.lockService?.updateCharacteristic( - this.hap.Characteristic.LockCurrentState, - this.hap.Characteristic.LockCurrentState.SECURED, - ); - this.debugLog( - `${this.device.remoteType}: ` + `${this.accessory.displayName} updateCharacteristic LockTargetState: SECURED, LockCurrentState: SECURED`, - ); + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockTargetState, + this.hap.Characteristic.LockTargetState.SECURED); + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, + this.hap.Characteristic.LockCurrentState.SECURED); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` LockTargetState: SECURED, LockCurrentState: SECURED (${this.LockMechanism.On})`); } } - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Lock On: ${this.On}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Lock On: ${this.LockMechanism?.On}`); } else if (this.otherDeviceType === 'faucet') { - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Faucet!.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.Faucet!.On}`); } else { - if (this.On) { - this.faucetService?.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.ACTIVE); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.On}`); + if (this.Faucet!.On) { + this.Faucet!.Service.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.ACTIVE); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Faucet!.On}`); } else { - this.faucetService?.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.INACTIVE); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.On}`); + this.Faucet!.Service.updateCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.INACTIVE); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Faucet!.On}`); } } - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Faucet On: ${this.On}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Faucet On: ${this.Faucet!.On}`); } else if (this.otherDeviceType === 'fan') { - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Fan!.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.Fan!.On}`); } else { - if (this.On) { - this.fanService?.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + if (this.Fan!.On) { + this.Fan!.Service.updateCharacteristic(this.hap.Characteristic.On, this.Fan!.On); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.Fan!.On}`); } else { - this.fanService?.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + this.Fan!.Service.updateCharacteristic(this.hap.Characteristic.On, this.Fan!.On); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.Fan!.On}`); } } - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Fan On: ${this.On}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Fan On: ${this.Fan!.On}`); } else if (this.otherDeviceType === 'stateful') { - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.StatefulProgrammableSwitch!.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.StatefulProgrammableSwitch!.On}`); } else { - if (this.On) { - this.statefulProgrammableSwitchService?.updateCharacteristic( - this.hap.Characteristic.ProgrammableSwitchEvent, - this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, - ); - this.statefulProgrammableSwitchService?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, 1); - this.debugLog( - `${this.device.remoteType}: ` + - `${this.accessory.displayName} updateCharacteristic ProgrammableSwitchEvent: SINGLE, ProgrammableSwitchOutputState: 1`, - ); + if (this.StatefulProgrammableSwitch!.On) { + this.StatefulProgrammableSwitch!.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, + this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS); + this.StatefulProgrammableSwitch!.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, 1); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` ProgrammableSwitchEvent: SINGLE, ProgrammableSwitchOutputState: 1 (${this.StatefulProgrammableSwitch!.On})`); } else { - this.statefulProgrammableSwitchService?.updateCharacteristic( - this.hap.Characteristic.ProgrammableSwitchEvent, - this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, - ); - this.statefulProgrammableSwitchService?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, 0); - this.debugLog( - `${this.device.remoteType}: ` + - `${this.accessory.displayName} updateCharacteristic ProgrammableSwitchEvent: SINGLE, ProgrammableSwitchOutputState: 0`, - ); + this.StatefulProgrammableSwitch!.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, + this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS); + this.StatefulProgrammableSwitch!.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, 0); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` ProgrammableSwitchEvent: SINGLE, ProgrammableSwitchOutputState: 0 (${this.StatefulProgrammableSwitch!.On})`); } } - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} StatefulProgrammableSwitch On: ${this.On}`); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} StatefulProgrammableSwitch On: ${this.StatefulProgrammableSwitch!.On}`); } else if (this.otherDeviceType === 'switch') { - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Switch!.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.Switch!.On}`); } else { - this.switchService?.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + this.Switch!.Service.updateCharacteristic(this.hap.Characteristic.On, this.Switch!.On); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.Switch!.On}`); } } else { - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); + if (this.Outlet!.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.Outlet!.On}`); } else { - this.outletService?.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); + this.Outlet!.Service.updateCharacteristic(this.hap.Characteristic.On, this.Outlet!.On); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.Outlet!.On}`); } } } - async disablePushOnChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOn === undefined) { - this.disablePushOn = false; - } else { - this.disablePushOn = device.disablePushOn; - } - } - - async disablePushOffChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOff === undefined) { - this.disablePushOff = false; - } else { - this.disablePushOff = device.disablePushOff; - } - } - - async commandType(): Promise { - let commandType: string; - if (this.device.commandType && this.device.customize) { - commandType = this.device.commandType; - } else if (this.device.customize) { - commandType = 'customize'; - } else { - commandType = 'command'; - } - return commandType; - } - - async commandOn(): Promise { - let command: string; - if (this.device.customize && this.device.customOn) { - command = this.device.customOn; - } else { - command = 'turnOn'; - } - return command; - } - - async commandOff(): Promise { - let command: string; - if (this.device.customize && this.device.customOff) { - command = this.device.customOff; - } else { - command = 'turnOff'; - } - return command; - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - break; - case 171: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - break; - case 190: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, but ` - + `the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.remoteType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); - } - } - async apiError(e: any): Promise { if (this.otherDeviceType === 'garagedoor') { - this.garageDoorService?.updateCharacteristic(this.hap.Characteristic.TargetDoorState, e); - this.garageDoorService?.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, e); - this.garageDoorService?.updateCharacteristic(this.hap.Characteristic.ObstructionDetected, e); + if (this.GarageDoor) { + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, e); + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, e); + this.GarageDoor.Service.updateCharacteristic(this.hap.Characteristic.ObstructionDetected, e); + } } else if (this.otherDeviceType === 'door') { - this.doorService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); - this.doorService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); - this.doorService?.updateCharacteristic(this.hap.Characteristic.PositionState, e); + if (this.Door) { + this.Door.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); + this.Door.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); + this.Door.Service.updateCharacteristic(this.hap.Characteristic.PositionState, e); + } } else if (this.otherDeviceType === 'window') { - this.windowService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); - this.windowService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); - this.windowService?.updateCharacteristic(this.hap.Characteristic.PositionState, e); + if (this.Window) { + this.Window.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); + this.Window.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); + this.Window.Service.updateCharacteristic(this.hap.Characteristic.PositionState, e); + } } else if (this.otherDeviceType === 'windowcovering') { - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); - this.windowCoveringService?.updateCharacteristic(this.hap.Characteristic.PositionState, e); + if (this.WindowCovering) { + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.TargetPosition, e); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.CurrentPosition, e); + this.WindowCovering.Service.updateCharacteristic(this.hap.Characteristic.PositionState, e); + } } else if (this.otherDeviceType === 'lock') { - this.doorService?.updateCharacteristic(this.hap.Characteristic.LockTargetState, e); - this.doorService?.updateCharacteristic(this.hap.Characteristic.LockCurrentState, e); + if (this.LockMechanism) { + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockTargetState, e); + this.LockMechanism.Service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, e); + } } else if (this.otherDeviceType === 'faucet') { - this.faucetService?.updateCharacteristic(this.hap.Characteristic.Active, e); + if (this.Faucet) { + this.Faucet.Service.updateCharacteristic(this.hap.Characteristic.Active, e); + } } else if (this.otherDeviceType === 'fan') { - this.fanService?.updateCharacteristic(this.hap.Characteristic.On, e); + if (this.Fan) { + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.On, e); + } } else if (this.otherDeviceType === 'stateful') { - this.statefulProgrammableSwitchService?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, e); - this.statefulProgrammableSwitchService?.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, e); + if (this.StatefulProgrammableSwitch) { + this.StatefulProgrammableSwitch.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent, e); + this.StatefulProgrammableSwitch.Service.updateCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState, e); + } } else if (this.otherDeviceType === 'switch') { - this.switchService?.updateCharacteristic(this.hap.Characteristic.On, e); + if (this.Switch) { + this.Switch.Service.updateCharacteristic(this.hap.Characteristic.On, e); + } } else { - this.outletService?.updateCharacteristic(this.hap.Characteristic.On, e); + if (this.Outlet) { + this.Outlet.Service.updateCharacteristic(this.hap.Characteristic.On, e); + } } } async removeOutletService(accessory: PlatformAccessory): Promise { - // If outletService still present, then remove first - this.outletService = this.accessory.getService(this.hap.Service.Outlet); - if (this.outletService) { + // If Outlet.Service still present, then remove first + if (this.Outlet?.Service) { + this.Outlet.Service = this.accessory.getService(this.hap.Service.Outlet) as Service; this.warnLog(`${this.device.remoteType}: ${accessory.displayName} Removing Leftover Outlet Service`); + accessory.removeService(this.Outlet.Service); } - accessory.removeService(this.outletService!); } async removeGarageDoorService(accessory: PlatformAccessory): Promise { - // If garageDoorService still present, then remove first - this.garageDoorService = this.accessory.getService(this.hap.Service.GarageDoorOpener); - if (this.garageDoorService) { + // If GarageDoor.Service still present, then remove first + if (this.GarageDoor?.Service) { + this.GarageDoor.Service = this.accessory.getService(this.hap.Service.GarageDoorOpener) as Service; this.warnLog(`${this.device.remoteType}: ${accessory.displayName} Removing Leftover Garage Door Service`); + accessory.removeService(this.GarageDoor.Service); } - accessory.removeService(this.garageDoorService!); } async removeDoorService(accessory: PlatformAccessory): Promise { - // If doorService still present, then remove first - this.doorService = this.accessory.getService(this.hap.Service.Door); - if (this.doorService) { + // If Door.Service still present, then remove first + if (this.Door?.Service) { + this.Door.Service = this.accessory.getService(this.hap.Service.Door) as Service; this.warnLog(`${this.device.remoteType}: ${accessory.displayName} Removing Leftover Door Service`); + accessory.removeService(this.Door.Service); } - accessory.removeService(this.doorService!); } async removeLockService(accessory: PlatformAccessory): Promise { - // If lockService still present, then remove first - this.lockService = this.accessory.getService(this.hap.Service.LockMechanism); - if (this.lockService) { + // If Lock.Service still present, then remove first + if (this.LockMechanism?.Service) { + this.LockMechanism.Service = this.accessory.getService(this.hap.Service.LockMechanism) as Service; this.warnLog(`${this.device.remoteType}: ${accessory.displayName} Removing Leftover Lock Service`); + accessory.removeService(this.LockMechanism.Service); } - accessory.removeService(this.lockService!); } async removeFaucetService(accessory: PlatformAccessory): Promise { - // If faucetService still present, then remove first - this.faucetService = this.accessory.getService(this.hap.Service.Faucet); - if (this.faucetService) { + // If Faucet.Service still present, then remove first + if (this.Faucet?.Service) { + this.Faucet.Service = this.accessory.getService(this.hap.Service.Faucet) as Service; this.warnLog(`${this.device.remoteType}: ${accessory.displayName} Removing Leftover Faucet Service`); + accessory.removeService(this.Faucet.Service); } - accessory.removeService(this.faucetService!); } async removeFanService(accessory: PlatformAccessory): Promise { - // If fanService still present, then remove first - this.fanService = this.accessory.getService(this.hap.Service.Fan); - if (this.fanService) { + // If Fan Service still present, then remove first + if (this.Fan?.Service) { + this.Fan.Service = this.accessory.getService(this.hap.Service.Fan) as Service; this.warnLog(`${this.device.remoteType}: ${accessory.displayName} Removing Leftover Fan Service`); + accessory.removeService(this.Fan.Service); } - accessory.removeService(this.fanService!); } async removeWindowService(accessory: PlatformAccessory): Promise { - // If windowService still present, then remove first - this.windowService = this.accessory.getService(this.hap.Service.Window); - if (this.windowService) { + // If Window.Service still present, then remove first + if (this.Window?.Service) { + this.Window.Service = this.accessory.getService(this.hap.Service.Window) as Service; this.warnLog(`${this.device.remoteType}: ${accessory.displayName} Removing Leftover Window Service`); + accessory.removeService(this.Window.Service); } - accessory.removeService(this.windowService!); } async removeWindowCoveringService(accessory: PlatformAccessory): Promise { - // If windowCoveringService still present, then remove first - this.windowCoveringService = this.accessory.getService(this.hap.Service.WindowCovering); - if (this.windowCoveringService) { + // If WindowCovering.Service still present, then remove first + if (this.WindowCovering?.Service) { + this.WindowCovering.Service = this.accessory.getService(this.hap.Service.WindowCovering) as Service; this.warnLog(`${this.device.remoteType}: ${accessory.displayName} Removing Leftover Window Covering Service`); + accessory.removeService(this.WindowCovering.Service); } - accessory.removeService(this.windowCoveringService!); } async removeStatefulProgrammableSwitchService(accessory: PlatformAccessory): Promise { - // If statefulProgrammableSwitchService still present, then remove first - this.statefulProgrammableSwitchService = this.accessory.getService(this.hap.Service.StatefulProgrammableSwitch); - if (this.statefulProgrammableSwitchService) { + // If StatefulProgrammableSwitch.Service still present, then remove first + if (this.StatefulProgrammableSwitch?.Service) { + this.StatefulProgrammableSwitch.Service = this.accessory.getService(this.hap.Service.StatefulProgrammableSwitch) as Service; this.warnLog(`${this.device.remoteType}: ${accessory.displayName} Removing Leftover Stateful Programmable Switch Service`); + accessory.removeService(this.StatefulProgrammableSwitch.Service); } - accessory.removeService(this.statefulProgrammableSwitchService!); } async removeSwitchService(accessory: PlatformAccessory): Promise { - // If switchService still present, then remove first - this.switchService = this.accessory.getService(this.hap.Service.Switch); - if (this.switchService) { + // If Switch.Service still present, then remove first + if (this.Switch?.Service) { + this.Switch.Service = this.accessory.getService(this.hap.Service.Switch) as Service; this.warnLog(`${this.device.remoteType}: ${accessory.displayName} Removing Leftover Switch Service`); + accessory.removeService(this.Switch.Service); } - accessory.removeService(this.switchService!); } - async deviceType(device: irdevice & irDevicesConfig): Promise { + async getOtherConfigSettings(device: irdevice & irDevicesConfig): Promise { if (!device.other?.deviceType && this.accessory.context.deviceType) { this.otherDeviceType = this.accessory.context.deviceType; this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Device Type: ${this.otherDeviceType}, from Accessory Cache.`); @@ -909,117 +881,4 @@ export class Others { this.warnLog(`${this.device.remoteType}: ${this.accessory.displayName} no deviceType set, using default deviceType: ${this.otherDeviceType}`); } } - - async deviceContext() { - if (this.On === undefined) { - this.On = true; - } else { - this.On = this.accessory.context.On; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async deviceConfig(device: irdevice & irDevicesConfig): Promise { - let config = {}; - if (device.other) { - config = device.other; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.customOn !== undefined) { - config['customOn'] = device.customOn; - } - if (device.customOff !== undefined) { - config['customOff'] = device.customOff; - } - if (device.customize !== undefined) { - config['customize'] = device.customize; - } - if (device.disablePushOn !== undefined) { - config['disablePushOn'] = device.disablePushOn; - } - if (device.disablePushOff !== undefined) { - config['disablePushOff'] = device.disablePushOff; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: irdevice & irDevicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; - } } diff --git a/src/irdevice/tv.ts b/src/irdevice/tv.ts index 9a58fb4e..5009f135 100644 --- a/src/irdevice/tv.ts +++ b/src/irdevice/tv.ts @@ -1,128 +1,128 @@ -import { CharacteristicValue, PlatformAccessory, Service, API, Logging, HAP } from 'homebridge'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * tv.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, irDevicesConfig, irdevice, SwitchBotPlatformConfig } from '../settings.js'; +import { Devices } from '../settings.js'; +import { irdeviceBase } from './irdevice.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { irDevicesConfig, irdevice } from '../settings.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class TV { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class TV extends irdeviceBase { // Services - tvService!: Service; - speakerService: Service; + private Television: { + Name: CharacteristicValue; + ConfiguredName: CharacteristicValue; + Service: Service; + Active: CharacteristicValue; + ActiveIdentifier: CharacteristicValue; + SleepDiscoveryMode: CharacteristicValue; + RemoteKey: CharacteristicValue; + }; + + private TelevisionSpeaker: { + Name: CharacteristicValue; + Service: Service; + Active: CharacteristicValue; + VolumeControlType: CharacteristicValue; + VolumeSelector: CharacteristicValue; + }; // Characteristic Values - Active!: CharacteristicValue; - ActiveIdentifier!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - - // Config - deviceLogging!: string; - disablePushOn?: boolean; - disablePushOff?: boolean; - disablePushDetail?: boolean; constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: irdevice & irDevicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: irdevice & irDevicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // default placeholders - this.deviceLogs(device); - this.deviceContext(); - this.disablePushOnChanges(device); - this.disablePushOffChanges(device); - this.disablePushDetailChanges(device); - this.deviceConfig(device); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Name, `${device.deviceName} ${device.remoteType}`) - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, device.remoteType) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // set the accessory category - const tvServiceCategory = `${accessory.displayName} ${device.remoteType}`; + super(platform, accessory, device); + + // Initialize Television Service + accessory.context.Television = accessory.context.Television ?? {}; + this.Television = { + Name: accessory.context.Television.Name ?? `${accessory.displayName} ${device.remoteType}`, + ConfiguredName: accessory.context.Television.ConfiguredName ?? `${accessory.displayName} ${device.remoteType}`, + Service: accessory.getService(this.hap.Service.Television) ?? accessory.addService(this.hap.Service.Television) as Service, + Active: accessory.context.Active ?? this.hap.Characteristic.Active.INACTIVE, + ActiveIdentifier: accessory.context.ActiveIdentifier ?? 1, + SleepDiscoveryMode: accessory.context.SleepDiscoveryMode ?? this.hap.Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE, + RemoteKey: accessory.context.RemoteKey ?? this.hap.Characteristic.RemoteKey.EXIT, + }; + accessory.context.Television = this.Television as object; + switch (device.remoteType) { case 'Speaker': case 'DIY Speaker': accessory.category = this.platform.api.hap.Categories.SPEAKER; - (this.tvService = accessory.getService(this.hap.Service.Television) - || accessory.addService(this.hap.Service.Television)), tvServiceCategory; break; case 'IPTV': case 'DIY IPTV': accessory.category = this.platform.api.hap.Categories.TV_STREAMING_STICK; - (this.tvService = accessory.getService(this.hap.Service.Television) - || accessory.addService(this.hap.Service.Television)), tvServiceCategory; break; case 'DVD': case 'DIY DVD': case 'Set Top Box': case 'DIY Set Top Box': accessory.category = this.platform.api.hap.Categories.TV_SET_TOP_BOX; - (this.tvService = accessory.getService(this.hap.Service.Television) - || accessory.addService(this.hap.Service.Television)), tvServiceCategory; break; default: accessory.category = this.platform.api.hap.Categories.TELEVISION; - - // get the Television service if it exists, otherwise create a new Television service - // you can create multiple services for each accessory - (this.tvService = accessory.getService(this.hap.Service.Television) - || accessory.addService(this.hap.Service.Television)), tvServiceCategory; } - this.tvService.getCharacteristic(this.hap.Characteristic.ConfiguredName); - - // set sleep discovery characteristic - this.tvService.setCharacteristic( - this.hap.Characteristic.SleepDiscoveryMode, - this.hap.Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE, - ); - - // handle on / off events using the Active characteristic - this.tvService.getCharacteristic(this.hap.Characteristic.Active).onSet(this.ActiveSet.bind(this)); - - this.tvService.setCharacteristic(this.hap.Characteristic.ActiveIdentifier, 1); - - // handle input source changes - this.tvService.getCharacteristic(this.hap.Characteristic.ActiveIdentifier).onSet(this.ActiveIdentifierSet.bind(this)); - - // handle remote control input - this.tvService.getCharacteristic(this.hap.Characteristic.RemoteKey).onSet(this.RemoteKeySet.bind(this)); - - /** - * Create a speaker service to allow volume control - */ - // create a new Television Speaker service - const speakerService = `${accessory.displayName} Speaker`; - (this.speakerService = accessory.getService(this.hap.Service.TelevisionSpeaker) - || accessory.addService(this.hap.Service.TelevisionSpeaker)), speakerService; - - this.speakerService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Speaker`); - if (!this.speakerService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.speakerService.addCharacteristic(this.hap.Characteristic.ConfiguredName, `${accessory.displayName} Speaker`); - } - this.speakerService + this.Television.Service + .setCharacteristic(this.hap.Characteristic.SleepDiscoveryMode, this.hap.Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE) + .setCharacteristic(this.hap.Characteristic.ConfiguredName, this.Television.ConfiguredName) + .getCharacteristic(this.hap.Characteristic.ConfiguredName); + + this.Television.Service + .setCharacteristic(this.hap.Characteristic.ActiveIdentifier, 1) + .getCharacteristic(this.hap.Characteristic.Active) + .onGet(() => { + return this.Television.Active; + }) + .onSet(this.ActiveSet.bind(this)); + + this.Television.Service + .getCharacteristic(this.hap.Characteristic.ActiveIdentifier) + .onGet(() => { + return this.Television.ActiveIdentifier; + }) + .onSet(this.ActiveIdentifierSet.bind(this)); + + this.Television.Service + .getCharacteristic(this.hap.Characteristic.RemoteKey) + .onGet(() => { + return this.Television.RemoteKey; + }) + .onSet(this.RemoteKeySet.bind(this)); + + // Initialize TelevisionSpeaker Service + accessory.context.TelevisionSpeaker = accessory.context.TelevisionSpeaker ?? {}; + this.TelevisionSpeaker = { + Name: accessory.context.TelevisionSpeaker.Name ?? `${accessory.displayName} ${device.remoteType} Speaker`, + Service: accessory.getService(this.hap.Service.TelevisionSpeaker) ?? accessory.addService(this.hap.Service.TelevisionSpeaker) as Service, + Active: accessory.context.Active ?? false, + VolumeControlType: accessory.context.VolumeControlType ?? this.hap.Characteristic.VolumeControlType.ABSOLUTE, + VolumeSelector: accessory.context.VolumeSelector ?? this.hap.Characteristic.VolumeSelector.INCREMENT, + }; + accessory.context.TelevisionSpeaker = this.TelevisionSpeaker as object; + + this.TelevisionSpeaker.Service + .setCharacteristic(this.hap.Characteristic.Name, this.TelevisionSpeaker.Name) .setCharacteristic(this.hap.Characteristic.Active, this.hap.Characteristic.Active.ACTIVE) - .setCharacteristic(this.hap.Characteristic.VolumeControlType, this.hap.Characteristic.VolumeControlType.ABSOLUTE); - - // handle volume control - this.speakerService.getCharacteristic(this.hap.Characteristic.VolumeSelector).onSet(this.VolumeSelectorSet.bind(this)); + .setCharacteristic(this.hap.Characteristic.VolumeControlType, this.hap.Characteristic.VolumeControlType.ABSOLUTE) + .getCharacteristic(this.hap.Characteristic.VolumeSelector) + .onGet(() => { + return this.TelevisionSpeaker.VolumeSelector; + }) + .onSet(this.VolumeSelectorSet.bind(this)); } async VolumeSelectorSet(value: CharacteristicValue): Promise { @@ -200,14 +200,14 @@ export class TV { async ActiveIdentifierSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} ActiveIdentifier: ${value}`); - this.ActiveIdentifier = value; + this.Television.ActiveIdentifier = value; } async ActiveSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active (value): ${value}`); - this.Active = value; - if (this.Active === this.hap.Characteristic.Active.ACTIVE) { + this.Television.Active = value; + if (this.Television.Active === this.hap.Characteristic.Active.ACTIVE) { await this.pushTvOnChanges(); } else { await this.pushTvOffChanges(); @@ -225,10 +225,9 @@ export class TV { * TV "command" "channelSub" "default" previous channel */ async pushTvOnChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushTvOnChanges Active: ${this.Active},` + ` disablePushOn: ${this.disablePushOn}`, - ); - if (this.Active === this.hap.Characteristic.Active.ACTIVE && !this.disablePushOn) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushTvOnChanges` + + ` Active: ${this.Television.Active}, disablePushOn: ${this.disablePushOn}`); + if (this.Television.Active === this.hap.Characteristic.Active.ACTIVE && !this.disablePushOn) { const commandType: string = await this.commandType(); const command: string = await this.commandOn(); const bodyChange = JSON.stringify({ @@ -241,10 +240,9 @@ export class TV { } async pushTvOffChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushTvOffChanges Active: ${this.Active},` + ` disablePushOff: ${this.disablePushOff}`, - ); - if (this.Active === this.hap.Characteristic.Active.INACTIVE && !this.disablePushOff) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushTvOffChanges` + + ` Active: ${this.Television.Active}, disablePushOff: ${this.disablePushOff}`); + if (this.Television.Active === this.hap.Characteristic.Active.INACTIVE && !this.disablePushOff) { const commandType: string = await this.commandType(); const command: string = await this.commandOff(); const bodyChange = JSON.stringify({ @@ -379,8 +377,10 @@ export class TV { this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -388,291 +388,37 @@ export class TV { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} failed pushTVChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} failed pushTVChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.warnLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`, - ); + this.warnLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`); } } async updateHomeKitCharacteristics(): Promise { // Active - if (this.Active === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${this.Active}`); + if (this.Television.Active === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${this.Television.Active}`); } else { - this.accessory.context.Active = this.Active; - this.tvService?.updateCharacteristic(this.hap.Characteristic.Active, this.Active); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Active}`); + this.accessory.context.Active = this.Television.Active; + this.Television.Service.updateCharacteristic(this.hap.Characteristic.Active, this.Television.Active); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Television.Active}`); } // ActiveIdentifier - if (this.ActiveIdentifier === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} ActiveIdentifier: ${this.ActiveIdentifier}`); - } else { - this.accessory.context.ActiveIdentifier = this.ActiveIdentifier; - this.tvService?.updateCharacteristic(this.hap.Characteristic.ActiveIdentifier, this.ActiveIdentifier); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName}` + ` updateCharacteristic ActiveIdentifier: ${this.ActiveIdentifier}`); - } - } - - async disablePushOnChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOn === undefined) { - this.disablePushOn = false; - } else { - this.disablePushOn = device.disablePushOn; - } - } - - async disablePushOffChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOff === undefined) { - this.disablePushOff = false; - } else { - this.disablePushOff = device.disablePushOff; - } - } - - async disablePushDetailChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushDetail === undefined) { - this.disablePushDetail = false; - } else { - this.disablePushDetail = device.disablePushDetail; - } - } - - async commandType(): Promise { - let commandType: string; - if (this.device.customize) { - commandType = 'customize'; - } else { - commandType = 'command'; - } - return commandType; - } - - async commandOn(): Promise { - let command: string; - if (this.device.customize && this.device.customOn) { - command = this.device.customOn; + if (this.Television.ActiveIdentifier === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} ActiveIdentifier: ${this.Television.ActiveIdentifier}`); } else { - command = 'turnOn'; - } - return command; - } - - async commandOff(): Promise { - let command: string; - if (this.device.customize && this.device.customOff) { - command = this.device.customOff; - } else { - command = 'turnOff'; - } - return command; - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - break; - case 171: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - break; - case 190: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, but ` - + `the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.remoteType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); + this.accessory.context.ActiveIdentifier = this.Television.ActiveIdentifier; + this.Television.Service.updateCharacteristic(this.hap.Characteristic.ActiveIdentifier, this.Television.ActiveIdentifier); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic` + + ` ActiveIdentifier: ${this.Television.ActiveIdentifier}`); } } async apiError(e: any): Promise { - this.tvService.updateCharacteristic(this.hap.Characteristic.Active, e); - this.tvService.updateCharacteristic(this.hap.Characteristic.ActiveIdentifier, e); - } - - async deviceContext() { - if (this.Active === undefined) { - this.Active = this.hap.Characteristic.Active.INACTIVE; - } else { - this.Active = this.accessory.context.Active; - } - if (this.ActiveIdentifier === undefined) { - this.ActiveIdentifier = 1; - } else { - this.ActiveIdentifier = this.accessory.context.ActiveIdentifier; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async deviceConfig(device: irdevice & irDevicesConfig): Promise { - let config = {}; - if (device.irtv) { - config = device.irtv; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.customOn !== undefined) { - config['customOn'] = device.customOn; - } - if (device.customOff !== undefined) { - config['customOff'] = device.customOff; - } - if (device.customize !== undefined) { - config['customize'] = device.customize; - } - if (device.disablePushOn !== undefined) { - config['disablePushOn'] = device.disablePushOn; - } - if (device.disablePushOff !== undefined) { - config['disablePushOff'] = device.disablePushOff; - } - if (device.disablePushDetail !== undefined) { - config['disablePushDetail'] = device.disablePushDetail; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: irdevice & irDevicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.Television.Service.updateCharacteristic(this.hap.Characteristic.Active, e); + this.Television.Service.updateCharacteristic(this.hap.Characteristic.ActiveIdentifier, e); } } diff --git a/src/irdevice/vacuumcleaner.ts b/src/irdevice/vacuumcleaner.ts index 9b0e58e2..ffad80ad 100644 --- a/src/irdevice/vacuumcleaner.ts +++ b/src/irdevice/vacuumcleaner.ts @@ -1,74 +1,59 @@ -import { CharacteristicValue, PlatformAccessory, Service, API, Logging, HAP } from 'homebridge'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * vacuumcleaner.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, irDevicesConfig, irdevice, SwitchBotPlatformConfig } from '../settings.js'; +import { Devices } from '../settings.js'; +import { irdeviceBase } from './irdevice.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { irDevicesConfig, irdevice } from '../settings.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class VacuumCleaner { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class VacuumCleaner extends irdeviceBase { // Services - switchService!: Service; - - // Characteristic Values - On!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - - // Config - deviceLogging!: string; - disablePushOn?: boolean; - disablePushOff?: boolean; + private Switch: { + Name: CharacteristicValue; + Service: Service; + On: CharacteristicValue; + }; constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: irdevice & irDevicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: irdevice & irDevicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // default placeholders - this.deviceLogs(device); - this.deviceContext(); - this.disablePushOnChanges(device); - this.disablePushOffChanges(device); - this.deviceConfig(device); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, device.remoteType) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Television service if it exists, otherwise create a new Television service - // you can create multiple services for each accessory - const switchService = `${accessory.displayName} ${device.remoteType}`; - (this.switchService = accessory.getService(this.hap.Service.Switch) - || accessory.addService(this.hap.Service.Switch)), switchService; - - this.switchService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.switchService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.switchService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - - // handle on / off events using the On characteristic - this.switchService.getCharacteristic(this.hap.Characteristic.On).onSet(this.OnSet.bind(this)); + super(platform, accessory, device); + + // Initialize Switch Service + accessory.context.Switch = accessory.context.Switch ?? {}; + this.Switch = { + Name: accessory.context.Switch.Name ?? `${accessory.displayName} ${device.remoteType}`, + Service: accessory.getService(this.hap.Service.Switch) ?? accessory.addService(this.hap.Service.Switch) as Service, + On: accessory.context.On ?? false, + }; + accessory.context.Switch = this.Switch as object; + + this.Switch.Service + .setCharacteristic(this.hap.Characteristic.Name, this.Switch.Name) + .getCharacteristic(this.hap.Characteristic.On) + .onGet(() => { + return this.Switch.On; + }) + .onSet(this.OnSet.bind(this)); } async OnSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${value}`); - this.On = value; - if (this.On) { + // Set the requested state + this.Switch.On = value; + if (this.Switch.On) { await this.pushOnChanges(); } else { await this.pushOffChanges(); @@ -82,8 +67,9 @@ export class VacuumCleaner { * Vacuum Cleaner "command" "turnOn" "default" set to ON state */ async pushOnChanges(): Promise { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushOnChanges On: ${this.On},` + ` disablePushOn: ${this.disablePushOn}`); - if (this.On && !this.disablePushOn) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushOnChanges` + + ` On: ${this.Switch.On}, disablePushOn: ${this.disablePushOn}`); + if (this.Switch.On && !this.disablePushOn) { const commandType: string = await this.commandType(); const command: string = await this.commandOn(); const bodyChange = JSON.stringify({ @@ -96,10 +82,9 @@ export class VacuumCleaner { } async pushOffChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushOffChanges On: ${this.On},` + ` disablePushOff: ${this.disablePushOff}`, - ); - if (!this.On && !this.disablePushOff) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushOffChanges` + + ` On: ${this.Switch.On}, disablePushOff: ${this.disablePushOff}`); + if (!this.Switch.On && !this.disablePushOff) { const commandType: string = await this.commandType(); const command: string = await this.commandOff(); const bodyChange = JSON.stringify({ @@ -126,8 +111,10 @@ export class VacuumCleaner { this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -135,266 +122,27 @@ export class VacuumCleaner { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} failed pushChange` + + ` with ${this.device.connectionType} Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.warnLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`, - ); + this.warnLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`); } } async updateHomeKitCharacteristics(): Promise { // On - if (this.On === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.On}`); - } else { - this.accessory.context.On = this.On; - this.switchService?.updateCharacteristic(this.hap.Characteristic.On, this.On); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.On}`); - } - } - - async disablePushOnChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOn === undefined) { - this.disablePushOn = false; - } else { - this.disablePushOn = device.disablePushOn; - } - } - - async disablePushOffChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOff === undefined) { - this.disablePushOff = false; + if (this.Switch.On === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} On: ${this.Switch.On}`); } else { - this.disablePushOff = device.disablePushOff; - } - } - - async commandType(): Promise { - let commandType: string; - if (this.device.customize) { - commandType = 'customize'; - } else { - commandType = 'command'; - } - return commandType; - } - - async commandOn(): Promise { - let command: string; - if (this.device.customize && this.device.customOn) { - command = this.device.customOn; - } else { - command = 'turnOn'; - } - return command; - } - - async commandOff(): Promise { - let command: string; - if (this.device.customize && this.device.customOff) { - command = this.device.customOff; - } else { - command = 'turnOff'; - } - return command; - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - break; - case 171: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - break; - case 190: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, but ` - + `the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.remoteType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); + this.accessory.context.On = this.Switch.On; + this.Switch.Service.updateCharacteristic(this.hap.Characteristic.On, this.Switch.On); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic On: ${this.Switch.On}`); } } async apiError(e: any): Promise { - this.switchService.updateCharacteristic(this.hap.Characteristic.On, e); - } - - async deviceContext() { - if (this.On === undefined) { - this.On = false; - } else { - this.On = this.accessory.context.On; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async deviceConfig(device: irdevice & irDevicesConfig): Promise { - let config = {}; - if (device.irvc) { - config = device.irvc; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.customOn !== undefined) { - config['customOn'] = device.customOn; - } - if (device.customOff !== undefined) { - config['customOff'] = device.customOff; - } - if (device.customize !== undefined) { - config['customize'] = device.customize; - } - if (device.disablePushOn !== undefined) { - config['disablePushOn'] = device.disablePushOn; - } - if (device.disablePushOff !== undefined) { - config['disablePushOff'] = device.disablePushOff; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); - } - } - - async deviceLogs(device: irdevice & irDevicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.Switch.Service.updateCharacteristic(this.hap.Characteristic.On, e); } } diff --git a/src/irdevice/waterheater.ts b/src/irdevice/waterheater.ts index 0edd3e7e..56fea9d3 100644 --- a/src/irdevice/waterheater.ts +++ b/src/irdevice/waterheater.ts @@ -1,82 +1,64 @@ -import { CharacteristicValue, PlatformAccessory, Service, API, Logging, HAP } from 'homebridge'; +/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * waterheater.ts: @switchbot/homebridge-switchbot. + */ import { request } from 'undici'; -import { SwitchBotPlatform } from '../platform.js'; -import { Devices, irDevicesConfig, irdevice, SwitchBotPlatformConfig } from '../settings.js'; +import { Devices } from '../settings.js'; +import { irdeviceBase } from './irdevice.js'; + +import type { SwitchBotPlatform } from '../platform.js'; +import type { irDevicesConfig, irdevice } from '../settings.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class WaterHeater { - public readonly api: API; - public readonly log: Logging; - public readonly config!: SwitchBotPlatformConfig; - protected readonly hap: HAP; +export class WaterHeater extends irdeviceBase { // Services - valveService!: Service; - - // Characteristic Values - Active!: CharacteristicValue; - FirmwareRevision!: CharacteristicValue; - - // Config - deviceLogging!: string; - disablePushOn?: boolean; - disablePushOff?: boolean; + private Valve: { + Name: CharacteristicValue; + Service: Service; + Active: CharacteristicValue; + }; constructor( - private readonly platform: SwitchBotPlatform, - private accessory: PlatformAccessory, - public device: irdevice & irDevicesConfig, + readonly platform: SwitchBotPlatform, + accessory: PlatformAccessory, + device: irdevice & irDevicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - // default placeholders - this.deviceLogs(device); - this.deviceContext(); - this.disablePushOnChanges(device); - this.disablePushOffChanges(device); - this.deviceConfig(device); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'SwitchBot') - .setCharacteristic(this.hap.Characteristic.Model, device.remoteType) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceId) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // get the Television service if it exists, otherwise create a new Television service - // you can create multiple services for each accessory - const valveService = `${accessory.displayName} ${device.remoteType}`; - (this.valveService = accessory.getService(this.hap.Service.Valve) - || accessory.addService(this.hap.Service.Valve)), valveService; - - this.valveService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.valveService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.valveService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); - } - - // set sleep discovery characteristic - this.valveService.setCharacteristic(this.hap.Characteristic.ValveType, this.hap.Characteristic.ValveType.GENERIC_VALVE); - - // handle on / off events using the Active characteristic - this.valveService.getCharacteristic(this.hap.Characteristic.Active).onSet(this.ActiveSet.bind(this)); + super(platform, accessory, device); + + // Initialize Switch Service + accessory.context.Valve = accessory.context.Valve ?? {}; + this.Valve = { + Name: accessory.context.Valve.Name ?? `${accessory.displayName} ${device.remoteType}`, + Service: accessory.getService(this.hap.Service.Valve) ?? accessory.addService(this.hap.Service.Valve) as Service, + Active: accessory.context.Active ?? this.hap.Characteristic.Active.INACTIVE, + }; + accessory.context.Valve = this.Valve as object; + + this.Valve.Service + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .setCharacteristic(this.hap.Characteristic.ValveType, this.hap.Characteristic.ValveType.GENERIC_VALVE) + .getCharacteristic(this.hap.Characteristic.Active) + .onGet(() => { + return this.Valve.Active; + }) + .onSet(this.ActiveSet.bind(this)); } async ActiveSet(value: CharacteristicValue): Promise { this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${value}`); - this.Active = value; - if (this.Active === this.hap.Characteristic.Active.ACTIVE) { + this.Valve.Active = value; + if (this.Valve.Active === this.hap.Characteristic.Active.ACTIVE) { await this.pushWaterHeaterOnChanges(); - this.valveService.setCharacteristic(this.hap.Characteristic.InUse, this.hap.Characteristic.InUse.IN_USE); + this.Valve.Service.setCharacteristic(this.hap.Characteristic.InUse, this.hap.Characteristic.InUse.IN_USE); } else { await this.pushWaterHeaterOffChanges(); - this.valveService.setCharacteristic(this.hap.Characteristic.InUse, this.hap.Characteristic.InUse.NOT_IN_USE); + this.Valve.Service.setCharacteristic(this.hap.Characteristic.InUse, this.hap.Characteristic.InUse.NOT_IN_USE); } } @@ -87,11 +69,9 @@ export class WaterHeater { * WaterHeater "command" "turnOn" "default" set to ON state */ async pushWaterHeaterOnChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushWaterHeaterOnChanges Active: ${this.Active},` + - ` disablePushOn: ${this.disablePushOn}`, - ); - if (this.Active === this.hap.Characteristic.Active.ACTIVE && !this.disablePushOn) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushWaterHeaterOnChanges Active: ${this.Valve.Active},` + + ` disablePushOn: ${this.disablePushOn}`); + if (this.Valve.Active === this.hap.Characteristic.Active.ACTIVE && !this.disablePushOn) { const commandType: string = await this.commandType(); const command: string = await this.commandOn(); const bodyChange = JSON.stringify({ @@ -104,11 +84,9 @@ export class WaterHeater { } async pushWaterHeaterOffChanges(): Promise { - this.debugLog( - `${this.device.remoteType}: ${this.accessory.displayName} pushWaterHeaterOffChanges Active: ${this.Active},` + - ` disablePushOff: ${this.disablePushOff}`, - ); - if (this.Active === this.hap.Characteristic.Active.INACTIVE && !this.disablePushOff) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} pushWaterHeaterOffChanges Active: ${this.Valve.Active},` + + ` disablePushOff: ${this.disablePushOff}`); + if (this.Valve.Active === this.hap.Characteristic.Active.INACTIVE && !this.disablePushOff) { const commandType: string = await this.commandType(); const command: string = await this.commandOff(); const bodyChange = JSON.stringify({ @@ -135,8 +113,10 @@ export class WaterHeater { this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus: ${JSON.stringify(deviceStatus)}`); this.debugWarnLog(`${this.device.remoteType}: ${this.accessory.displayName} deviceStatus statusCode: ${deviceStatus.statusCode}`); if ((statusCode === 200 || statusCode === 100) && (deviceStatus.statusCode === 200 || deviceStatus.statusCode === 100)) { - this.debugErrorLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + this.debugSuccessLog(`${this.device.remoteType}: ${this.accessory.displayName} ` + `statusCode: ${statusCode} & deviceStatus StatusCode: ${deviceStatus.statusCode}`); + this.successLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` request to SwitchBot API, body: ${JSON.stringify(JSON.parse(bodyChange))} sent successfully`); this.updateHomeKitCharacteristics(); } else { this.statusCode(statusCode); @@ -144,266 +124,27 @@ export class WaterHeater { } } catch (e: any) { this.apiError(e); - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + - ` Connection, Error Message: ${JSON.stringify(e.message)}`, - ); + this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} failed pushChanges with ${this.device.connectionType}` + + ` Connection, Error Message: ${JSON.stringify(e.message)}`); } } else { - this.warnLog( - `${this.device.remoteType}: ${this.accessory.displayName}` + - ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`, - ); + this.warnLog(`${this.device.remoteType}: ${this.accessory.displayName}` + + ` Connection Type: ${this.device.connectionType}, commands will not be sent to OpenAPI`); } } async updateHomeKitCharacteristics(): Promise { // Active - if (this.Active === undefined) { - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${this.Active}`); - } else { - this.accessory.context.Active = this.Active; - this.valveService?.updateCharacteristic(this.hap.Characteristic.Active, this.Active); - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Active}`); - } - } - - async disablePushOnChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOn === undefined) { - this.disablePushOn = false; - } else { - this.disablePushOn = device.disablePushOn; - } - } - - async disablePushOffChanges(device: irdevice & irDevicesConfig): Promise { - if (device.disablePushOff === undefined) { - this.disablePushOff = false; - } else { - this.disablePushOff = device.disablePushOff; - } - } - - async commandType(): Promise { - let commandType: string; - if (this.device.customize) { - commandType = 'customize'; - } else { - commandType = 'command'; - } - return commandType; - } - - async commandOn(): Promise { - let command: string; - if (this.device.customize && this.device.customOn) { - command = this.device.customOn; - } else { - command = 'turnOn'; - } - return command; - } - - async commandOff(): Promise { - let command: string; - if (this.device.customize && this.device.customOff) { - command = this.device.customOff; + if (this.Valve.Active === undefined) { + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Active: ${this.Valve.Active}`); } else { - command = 'turnOff'; - } - return command; - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 151: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command not supported by this deviceType, statusCode: ${statusCode}`); - break; - case 152: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device not found, statusCode: ${statusCode}`); - break; - case 160: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Command is not supported, statusCode: ${statusCode}`); - break; - case 161: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Device is offline, statusCode: ${statusCode}`); - break; - case 171: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Hub Device is offline, statusCode: ${statusCode}. ` + - `Hub: ${this.device.hubDeviceId}`, - ); - break; - case 190: - this.errorLog( - `${this.device.remoteType}: ${this.accessory.displayName} Device internal error due to device states not synchronized with server,` + - ` Or command format is invalid, statusCode: ${statusCode}`, - ); - break; - case 100: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Command successfully sent, statusCode: ${statusCode}`); - break; - case 200: - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Request successful, statusCode: ${statusCode}`); - break; - case 400: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Bad Request, The client has issued an invalid request. ` - + `This is commonly used to specify validation errors in a request payload, statusCode: ${statusCode}`); - break; - case 401: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unauthorized, Authorization for the API is required, ` - + `but the request has not been authenticated, statusCode: ${statusCode}`); - break; - case 403: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` - + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); - break; - case 404: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Found, Specifies the requested path does not exist, ` - + `statusCode: ${statusCode}`); - break; - case 406: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Not Acceptable, The client has requested a MIME type via ` - + `the Accept header for a value not supported by the server, statusCode: ${statusCode}`); - break; - case 415: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unsupported Media Type, The client has defined a contentType ` - + `header that is not supported by the server, statusCode: ${statusCode}`); - break; - case 422: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Unprocessable Entity, The client has made a valid request, but ` - + `the server cannot process it. This is often used for APIs for which certain limits have been exceeded, statusCode: ${statusCode}`); - break; - case 429: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Too Many Requests, The client has exceeded the number of ` - + `requests allowed for a given time window, statusCode: ${statusCode}`); - break; - case 500: - this.errorLog(`${this.device.remoteType}: ${this.accessory.displayName} Internal Server Error, An unexpected error on the SmartThings ` - + `servers has occurred. These errors should be rare, statusCode: ${statusCode}`); - break; - default: - this.infoLog( - `${this.device.remoteType}: ${this.accessory.displayName} Unknown statusCode: ` + - `${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`, - ); + this.accessory.context.Active = this.Valve.Active; + this.Valve.Service.updateCharacteristic(this.hap.Characteristic.Active, this.Valve.Active); + this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} updateCharacteristic Active: ${this.Valve.Active}`); } } async apiError({ e }: { e: any }): Promise { - this.valveService.updateCharacteristic(this.hap.Characteristic.Active, e); - } - - async deviceContext(): Promise { - if (this.Active === undefined) { - this.Active = this.hap.Characteristic.Active.INACTIVE; - } else { - this.Active = this.accessory.context.Active; - } - if (this.FirmwareRevision === undefined) { - this.FirmwareRevision = this.platform.version; - this.accessory.context.FirmwareRevision = this.FirmwareRevision; - } - } - - async deviceConfig(device: irdevice & irDevicesConfig): Promise { - let config = {}; - if (device.irwh) { - config = device.irwh; - } - if (device.logging !== undefined) { - config['logging'] = device.logging; - } - if (device.connectionType !== undefined) { - config['connectionType'] = device.connectionType; - } - if (device.external !== undefined) { - config['external'] = device.external; - } - if (device.customOn !== undefined) { - config['customOn'] = device.customOn; - } - if (device.customOff !== undefined) { - config['customOff'] = device.customOff; - } - if (device.customize !== undefined) { - config['customize'] = device.customize; - } - if (device.disablePushOn !== undefined) { - config['disablePushOn'] = device.disablePushOn; - } - if (device.disablePushOff !== undefined) { - config['disablePushOff'] = device.disablePushOff; - } - if (Object.entries(config).length !== 0) { - this.debugWarnLog({ log: [`${this.device.remoteType}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`] }); - } - } - - async deviceLogs(device: irdevice & irDevicesConfig): Promise { - if (this.platform.debugMode) { - this.deviceLogging = this.accessory.context.logging = 'debugMode'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (device.logging) { - this.deviceLogging = this.accessory.context.logging = device.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.platform.config.options?.logging) { - this.deviceLogging = this.accessory.context.logging = this.platform.config.options?.logging; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = this.accessory.context.logging = 'standard'; - this.debugLog(`${this.device.remoteType}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + this.Valve.Service.updateCharacteristic(this.hap.Characteristic.Active, e); } } diff --git a/src/platform.ts b/src/platform.ts index c35f4378..0cae14eb 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,26 +1,27 @@ -/* Copyright(C) 2017-2023, donavanbecker (https://github.com/donavanbecker). All rights reserved. +/* Copyright(C) 2017-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. * * platform.ts: @switchbot/homebridge-switchbot platform class. */ -import { API, DynamicPlatformPlugin, Logging, PlatformAccessory } from 'homebridge'; -import { PLATFORM_NAME, PLUGIN_NAME, irdevice, device, SwitchBotPlatformConfig, devicesConfig, irDevicesConfig, Devices } from './settings.js'; +import { Hub } from './device/hub.js'; import { Bot } from './device/bot.js'; import { Plug } from './device/plug.js'; import { Lock } from './device/lock.js'; import { Meter } from './device/meter.js'; import { Motion } from './device/motion.js'; -import { Hub } from './device/hub.js'; import { Contact } from './device/contact.js'; import { Curtain } from './device/curtain.js'; import { IOSensor } from './device/iosensor.js'; import { MeterPlus } from './device/meterplus.js'; import { ColorBulb } from './device/colorbulb.js'; -import { CeilingLight } from './device/ceilinglight.js'; import { StripLight } from './device/lightstrip.js'; import { Humidifier } from './device/humidifier.js'; +import { CeilingLight } from './device/ceilinglight.js'; +import { WaterDetector } from './device/waterdetector.js'; import { RobotVacuumCleaner } from './device/robotvacuumcleaner.js'; + +import { Fan } from './device/fan.js'; import { TV } from './irdevice/tv.js'; -import { Fan } from './irdevice/fan.js'; +import { IRFan } from './irdevice/fan.js'; import { Light } from './irdevice/light.js'; import { Others } from './irdevice/other.js'; import { Camera } from './irdevice/camera.js'; @@ -29,18 +30,25 @@ import { AirPurifier } from './irdevice/airpurifier.js'; import { WaterHeater } from './irdevice/waterheater.js'; import { VacuumCleaner } from './irdevice/vacuumcleaner.js'; import { AirConditioner } from './irdevice/airconditioner.js'; -import * as http from 'http'; + import { Buffer } from 'buffer'; import { request } from 'undici'; -import { MqttClient } from 'mqtt'; +import asyncmqtt from 'async-mqtt'; +import { sleep } from './utils.js'; +import { createServer } from 'http'; import { queueScheduler } from 'rxjs'; import fakegato from 'fakegato-history'; -import asyncmqtt from 'async-mqtt'; import crypto, { randomUUID } from 'crypto'; import { readFileSync, writeFileSync } from 'fs'; -import hbLib from 'homebridge-lib'; - +import { EveHomeKitTypes } from 'homebridge-lib/EveHomeKitTypes'; +import { PLATFORM_NAME, PLUGIN_NAME, Devices, setupWebhook, updateWebhook, deleteWebhook, queryWebhook } from './settings.js'; +import type { UrlObject } from 'url'; +import type { MqttClient } from 'mqtt'; +import type { Dispatcher } from 'undici'; +import type { Server, IncomingMessage, ServerResponse } from 'http'; +import type { API, DynamicPlatformPlugin, Logging, PlatformAccessory } from 'homebridge'; +import type { irdevice, device, SwitchBotPlatformConfig, devicesConfig, irDevicesConfig } from './settings.js'; /** * HomebridgePlatform @@ -55,11 +63,13 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { version!: string; Logging?: string; debugMode!: boolean; + maxRetries!: number; + delayBetweenRetries!: number; platformConfig!: SwitchBotPlatformConfig['options']; platformLogging!: SwitchBotPlatformConfig['logging']; config!: SwitchBotPlatformConfig; - webhookEventListener: http.Server | null = null; + webhookEventListener: Server | null = null; mqttClient: MqttClient | null = null; public readonly fakegatoAPI: any; @@ -71,9 +81,11 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { config: SwitchBotPlatformConfig, api: API, ) { + // initialize this.accessories = []; this.api = api; this.log = log; + // only load if configured if (!config) { return; @@ -86,10 +98,13 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { credentials: config.credentials as object, options: config.options as object, }; - this.platformLogging = this.config.options?.logging ?? 'standard'; - this.platformConfigOptions(); - this.platformLogs(); + + // Plugin Configuration + this.getPlatformConfigSettings(); + this.getPlatformLogSettings(); this.getVersion(); + + // Finish initializing the platform this.debugLog(`Finished initializing platform: ${config.name}`); // verify the config @@ -103,7 +118,6 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { } // import fakegato-history module and EVE characteristics - const { EveHomeKitTypes } = hbLib; this.fakegatoAPI = fakegato(api); this.eve = new EveHomeKitTypes(api); @@ -118,7 +132,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { if (this.config.credentials?.openToken && !this.config.credentials.token) { await this.updateToken(); } else if (this.config.credentials?.token && !this.config.credentials?.secret) { - // eslint-disable-next-line no-useless-escape + this.errorLog('"secret" config is not populated, you must populate then please restart Homebridge.'); } else { this.discoverDevices(); @@ -172,7 +186,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { const xurl = new URL(url); const port = Number(xurl.port); const path = xurl.pathname; - this.webhookEventListener = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { + this.webhookEventListener = createServer((request: IncomingMessage, response: ServerResponse) => { try { if (request.url === path && request.method === 'POST') { request.on('data', async (data) => { @@ -209,16 +223,15 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { } try { - const { body, statusCode } = await request( - 'https://api.switch-bot.com/v1.1/webhook/setupWebhook', { - method: 'POST', - headers: this.generateHeaders(), - body: JSON.stringify({ - 'action': 'setupWebhook', - 'url': url, - 'deviceList': 'ALL', - }), - }); + const { body, statusCode } = await request(setupWebhook, { + method: 'POST', + headers: this.generateHeaders(), + body: JSON.stringify({ + 'action': 'setupWebhook', + 'url': url, + 'deviceList': 'ALL', + }), + }); const response: any = await body.json(); this.debugLog(`setupWebhook: url:${url}`); this.debugLog(`setupWebhook: body:${JSON.stringify(response)}`); @@ -232,18 +245,15 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { } try { - const { body, statusCode } = await request( - 'https://api.switch-bot.com/v1.1/webhook/updateWebhook', { - method: 'POST', - headers: this.generateHeaders(), - body: JSON.stringify({ - 'action': 'updateWebhook', - 'config': { - 'url': url, - 'enable': true, - }, - }), - }); + const { body, statusCode } = await request(updateWebhook, { + method: 'POST', headers: this.generateHeaders(), body: JSON.stringify({ + 'action': 'updateWebhook', + 'config': { + 'url': url, + 'enable': true, + }, + }), + }); const response: any = await body.json(); this.debugLog(`updateWebhook: url:${url}`); this.debugLog(`updateWebhook: body:${JSON.stringify(response)}`); @@ -256,14 +266,13 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { } try { - const { body, statusCode } = await request( - 'https://api.switch-bot.com/v1.1/webhook/queryWebhook', { - method: 'POST', - headers: this.generateHeaders(), - body: JSON.stringify({ - 'action': 'queryUrl', - }), - }); + const { body, statusCode } = await request(queryWebhook, { + method: 'POST', + headers: this.generateHeaders(), + body: JSON.stringify({ + 'action': 'queryUrl', + }), + }); const response: any = await body.json(); this.debugLog(`queryWebhook: body:${JSON.stringify(response)}`); this.debugLog(`queryWebhook: statusCode:${statusCode}`); @@ -278,15 +287,14 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { this.api.on('shutdown', async () => { try { - const { body, statusCode } = await request( - 'https://api.switch-bot.com/v1.1/webhook/deleteWebhook', { - method: 'POST', - headers: this.generateHeaders(), - body: JSON.stringify({ - 'action': 'deleteWebhook', - 'url': url, - }), - }); + const { body, statusCode } = await request(deleteWebhook, { + method: 'POST', + headers: this.generateHeaders(), + body: JSON.stringify({ + 'action': 'deleteWebhook', + 'url': url, + }), + }); const response: any = await body.json(); this.debugLog(`deleteWebhook: url:${url}`); this.debugLog(`deleteWebhook: body:${JSON.stringify(response)}`); @@ -382,6 +390,21 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { this.debugWarnLog('Using Default Push Rate.'); } + if (!this.config.options.maxRetries) { + this.config.options.maxRetries = 5; + this.debugWarnLog('Using Default Max Retries.'); + } else { + this.maxRetries = this.config.options.maxRetries; + } + + if (!this.config.options.delayBetweenRetries) { + // default 3 seconds + this.config.options!.delayBetweenRetries! = 3000; + this.debugWarnLog('Using Default Delay Between Retries.'); + } else { + this.delayBetweenRetries = this.config.options.delayBetweenRetries * 1000; + } + if (!this.config.credentials && !this.config.options) { this.debugWarnLog('Missing Credentials'); } else if (this.config.credentials && !this.config.credentials.notice) { @@ -431,18 +454,12 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { } // Move openToken to token if (!this.config.credentials.secret) { - // eslint-disable-next-line no-useless-escape, max-len - this.warnLog( - 'This plugin has been updated to use OpenAPI v1.1, config is set with openToken, "openToken" cconfig has been moved to the "token" config', - ); - // eslint-disable-next-line no-useless-escape + this.warnLog('This plugin has been updated to use OpenAPI v1.1, config is set with openToken,' + + ' "openToken" cconfig has been moved to the "token" config'); this.errorLog('"secret" config is not populated, you must populate then please restart Homebridge.'); } else { - // eslint-disable-next-line no-useless-escape, max-len - this.warnLog( - 'This plugin has been updated to use OpenAPI v1.1, config is set with openToken, ' - + '"openToken" config has been moved to the "token" config, please restart Homebridge.', - ); + this.warnLog('This plugin has been updated to use OpenAPI v1.1, config is set with openToken, ' + + '"openToken" config has been moved to the "token" config, please restart Homebridge.'); } // set the refresh token @@ -491,44 +508,98 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { */ async discoverDevices() { if (this.config.credentials?.token) { - try { - const { body, statusCode } = await request(Devices, { - headers: this.generateHeaders(), - }); - this.debugWarnLog(`statusCode: ${statusCode}`); - const devicesAPI: any = await body.json(); - this.debugWarnLog(`devicesAPI: ${JSON.stringify(devicesAPI)}`); - this.debugWarnLog(`devicesAPI Body: ${JSON.stringify(devicesAPI.body)}`); - this.debugWarnLog(`devicesAPI StatusCode: ${devicesAPI.statusCode}`); - if ((statusCode === 200 || statusCode === 100) && (devicesAPI.statusCode === 200 || devicesAPI.statusCode === 100)) { - this.debugErrorLog(`statusCode: ${statusCode} & devicesAPI StatusCode: ${devicesAPI.statusCode}`); - // SwitchBot Devices - const deviceLists = devicesAPI.body.deviceList; - this.debugWarnLog(`DeviceLists: ${JSON.stringify(deviceLists)}`); - this.debugWarnLog(`DeviceLists Length: ${deviceLists.length}`); - if (!this.config.options?.devices) { - this.debugLog(`SwitchBot Device Config Not Set: ${JSON.stringify(this.config.options?.devices)}`); - if (deviceLists.length === 0) { - this.debugLog(`SwitchBot API Currently Doesn't Have Any Devices With Cloud Services Enabled: ${JSON.stringify(devicesAPI.body)}`); - } else { - const devices = deviceLists.map((v: any) => v); - for (const device of devices) { - if (device.deviceType) { - if (device.configDeviceName) { - device.deviceName = device.configDeviceName; + let retryCount = 0; + const maxRetries = this.maxRetries; // Maximum number of retries + const delayBetweenRetries = this.delayBetweenRetries; // Delay between retries in milliseconds + this.debugWarnLog(`Retry Count: ${retryCount}`); + this.debugWarnLog(`Max Retries: ${maxRetries}`); + this.debugWarnLog(`Delay Between Retries: ${delayBetweenRetries}`); + while (retryCount < maxRetries) { + try { + const { body, statusCode } = await request(Devices, { + headers: this.generateHeaders(), + }); + this.debugWarnLog(`statusCode: ${statusCode}`); + const devicesAPI: any = await body.json(); + this.debugWarnLog(`devicesAPI: ${JSON.stringify(devicesAPI)}`); + this.debugWarnLog(`devicesAPI Body: ${JSON.stringify(devicesAPI.body)}`); + this.debugWarnLog(`devicesAPI StatusCode: ${devicesAPI.statusCode}`); + if ((statusCode === 200 || statusCode === 100) && (devicesAPI.statusCode === 200 || devicesAPI.statusCode === 100)) { + this.debugErrorLog(`statusCode: ${statusCode} & devicesAPI StatusCode: ${devicesAPI.statusCode}`); + // SwitchBot Devices + const deviceLists = devicesAPI.body.deviceList; + this.debugWarnLog(`DeviceLists: ${JSON.stringify(deviceLists)}`); + this.debugWarnLog(`DeviceLists Length: ${deviceLists.length}`); + if (!this.config.options?.devices) { + this.debugLog(`SwitchBot Device Config Not Set: ${JSON.stringify(this.config.options?.devices)}`); + if (deviceLists.length === 0) { + this.debugLog(`SwitchBot API Currently Doesn't Have Any Devices With Cloud Services Enabled: ${JSON.stringify(devicesAPI.body)}`); + } else { + const devices = deviceLists.map((v: any) => v); + for (const device of devices) { + if (device.deviceType) { + if (device.configDeviceName) { + device.deviceName = device.configDeviceName; + } + this.createDevice(device); + } + } + } + } else if (this.config.credentials?.token && this.config.options.devices) { + this.debugLog(`SwitchBot Device Config Set: ${JSON.stringify(this.config.options?.devices)}`); + if (deviceLists.length === 0) { + this.debugLog(`SwitchBot API Currently Doesn't Have Any Devices With Cloud Services Enabled: ${JSON.stringify(devicesAPI.body)}`); + } else { + const deviceConfigs = this.config.options?.devices; + + const mergeBydeviceId = (a1: { deviceId: string }[], a2: any[]) => + a1.map((itm: { deviceId: string }) => ({ + ...a2.find( + (item: { deviceId: string }) => + item.deviceId.toUpperCase().replace(/[^A-Z0-9]+/g, '') === itm.deviceId.toUpperCase().replace(/[^A-Z0-9]+/g, '') && item, + ), + ...itm, + })); + + const devices = mergeBydeviceId(deviceLists, deviceConfigs); + this.debugLog(`SwitchBot Devices: ${JSON.stringify(devices)}`); + for (const device of devices) { + if (!device.deviceType) { + device.deviceType = device.configDeviceType; + this.errorLog(`API has displaying no deviceType: ${device.deviceType}, So using configDeviceType: ${device.configDeviceType}`); + } + if (device.deviceType) { + if (device.configDeviceName) { + device.deviceName = device.configDeviceName; + } + this.createDevice(device); } - this.createDevice(device); } } + } else { + this.errorLog('SwitchBot Token Supplied, Issue with Auth.'); } - } else if (this.config.credentials?.token && this.config.options.devices) { - this.debugLog(`SwitchBot Device Config Set: ${JSON.stringify(this.config.options?.devices)}`); - if (deviceLists.length === 0) { - this.debugLog(`SwitchBot API Currently Doesn't Have Any Devices With Cloud Services Enabled: ${JSON.stringify(devicesAPI.body)}`); + if (devicesAPI.body.deviceList.length !== 0) { + this.infoLog(`Total SwitchBot Devices Found: ${devicesAPI.body.deviceList.length}`); } else { - const deviceConfigs = this.config.options?.devices; + this.debugLog(`Total SwitchBot Devices Found: ${devicesAPI.body.deviceList.length}`); + } - const mergeBydeviceId = (a1: { deviceId: string }[], a2: any[]) => + // IR Devices + const irDeviceLists = devicesAPI.body.infraredRemoteList; + if (!this.config.options?.irdevices) { + this.debugLog(`IR Device Config Not Set: ${JSON.stringify(this.config.options?.irdevices)}`); + const devices = irDeviceLists.map((v: any) => v); + for (const device of devices) { + if (device.remoteType) { + this.createIRDevice(device); + } + } + } else { + this.debugLog(`IR Device Config Set: ${JSON.stringify(this.config.options?.irdevices)}`); + const irDeviceConfig = this.config.options?.irdevices; + + const mergeIRBydeviceId = (a1: { deviceId: string }[], a2: any[]) => a1.map((itm: { deviceId: string }) => ({ ...a2.find( (item: { deviceId: string }) => @@ -537,73 +608,33 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { ...itm, })); - const devices = mergeBydeviceId(deviceLists, deviceConfigs); - this.debugLog(`SwitchBot Devices: ${JSON.stringify(devices)}`); + const devices = mergeIRBydeviceId(irDeviceLists, irDeviceConfig); + this.debugLog(`IR Devices: ${JSON.stringify(devices)}`); for (const device of devices) { - if (!device.deviceType) { - device.deviceType = device.configDeviceType; - this.errorLog(`API has displaying no deviceType: ${device.deviceType}, So using configDeviceType: ${device.configDeviceType}`); - } - if (device.deviceType) { - if (device.configDeviceName) { - device.deviceName = device.configDeviceName; - } - this.createDevice(device); - } - } - } - } else { - this.errorLog('SwitchBot Token Supplied, Issue with Auth.'); - } - if (devicesAPI.body.deviceList.length !== 0) { - this.infoLog(`Total SwitchBot Devices Found: ${devicesAPI.body.deviceList.length}`); - } else { - this.debugLog(`Total SwitchBot Devices Found: ${devicesAPI.body.deviceList.length}`); - } - - // IR Devices - const irDeviceLists = devicesAPI.body.infraredRemoteList; - if (!this.config.options?.irdevices) { - this.debugLog(`IR Device Config Not Set: ${JSON.stringify(this.config.options?.irdevices)}`); - const devices = irDeviceLists.map((v: any) => v); - for (const device of devices) { - if (device.remoteType) { this.createIRDevice(device); } } - } else { - this.debugLog(`IR Device Config Set: ${JSON.stringify(this.config.options?.irdevices)}`); - const irDeviceConfig = this.config.options?.irdevices; - - const mergeIRBydeviceId = (a1: { deviceId: string }[], a2: any[]) => - a1.map((itm: { deviceId: string }) => ({ - ...a2.find( - (item: { deviceId: string }) => - item.deviceId.toUpperCase().replace(/[^A-Z0-9]+/g, '') === itm.deviceId.toUpperCase().replace(/[^A-Z0-9]+/g, '') && item, - ), - ...itm, - })); - - const devices = mergeIRBydeviceId(irDeviceLists, irDeviceConfig); - this.debugLog(`IR Devices: ${JSON.stringify(devices)}`); - for (const device of devices) { - this.createIRDevice(device); + if (devicesAPI.body.infraredRemoteList.length !== 0) { + this.infoLog(`Total IR Devices Found: ${devicesAPI.body.infraredRemoteList.length}`); + } else { + this.debugLog(`Total IR Devices Found: ${devicesAPI.body.infraredRemoteList.length}`); } - } - if (devicesAPI.body.infraredRemoteList.length !== 0) { - this.infoLog(`Total IR Devices Found: ${devicesAPI.body.infraredRemoteList.length}`); + break; } else { - this.debugLog(`Total IR Devices Found: ${devicesAPI.body.infraredRemoteList.length}`); + this.statusCode(statusCode); + this.statusCode(devicesAPI.statusCode); + if (statusCode === 500) { + retryCount++; + this.infoLog(`statusCode: ${statusCode} Attempt ${retryCount} of ${maxRetries}`); + await sleep(delayBetweenRetries); + } } - } else { - this.statusCode(statusCode); - this.statusCode(devicesAPI.statusCode); + } catch (e: any) { + retryCount++; + this.debugErrorLog( + `Failed to Discover Devices, Error Message: ${JSON.stringify(e.message)}, Submit Bugs Here: ` + 'https://tinyurl.com/SwitchBotBug'); + this.debugErrorLog(`Failed to Discover Devices, Error: ${e}`); } - } catch (e: any) { - this.debugErrorLog( - `Failed to Discover Devices, Error Message: ${JSON.stringify(e.message)}, Submit Bugs Here: ` + 'https://tinyurl.com/SwitchBotBug', - ); - this.debugErrorLog(`Failed to Discover Devices, Error: ${e}`); } } else if (!this.config.credentials?.token && this.config.options?.devices) { this.debugLog(`SwitchBot Device Manual Config Set: ${JSON.stringify(this.config.options?.devices)}`); @@ -654,6 +685,10 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { this.debugLog(`Discovered ${device.deviceType}: ${device.deviceId}`); this.createIOSensor(device); break; + case 'Water Detector': + this.debugLog(`Discovered ${device.deviceType}: ${device.deviceId}`); + this.createWaterDetector(device); + break; case 'Motion Sensor': this.debugLog(`Discovered ${device.deviceType}: ${device.deviceId}`); this.createMotion(device); @@ -686,10 +721,12 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { this.debugLog(`Discovered ${device.deviceType}: ${device.deviceId}`); this.createColorBulb(device); break; + case 'K10+': case 'WoSweeper': case 'WoSweeperMini': case 'Robot Vacuum Cleaner S1': case 'Robot Vacuum Cleaner S1 Plus': + case 'Robot Vacuum Cleaner S10': this.debugLog(`Discovered ${device.deviceType}: ${device.deviceId}`); this.createRobotVacuumCleaner(device); break; @@ -702,17 +739,18 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { this.debugLog(`Discovered ${device.deviceType}: ${device.deviceId}`); this.createStripLight(device); break; - case 'Indoor Cam': + case 'Battery Circulator Fan': this.debugLog(`Discovered ${device.deviceType}: ${device.deviceId}`); - this.warnLog(`Device: ${device.deviceName} with Device Type: ${device.deviceType}, is currently not supported.`); + this.createFan(device); break; case 'Remote': - this.debugLog(`Discovered ${device.deviceType}: ${device.deviceId} is Not Supported.`); + case 'Indoor Cam': + case 'remote with screen+': + this.debugLog(`Discovered ${device.deviceType}: ${device.deviceId}, is currently not supported, device: ${JSON.stringify(device)}`); break; default: - this.warnLog(`Device: ${device.deviceName} with Device Type: ${device.deviceType}, is currently not supported.`); - // eslint-disable-next-line max-len - this.warnLog('Submit Feature Requests Here: ' + 'https://tinyurl.com/SwitchBotFeatureRequest'); + this.warnLog(`Device: ${device.deviceName} with Device Type: ${device.deviceType}, is currently not supported.`, + + `Submit Feature Requests Here: https://tinyurl.com/SwitchBotFeatureRequest, device: ${JSON.stringify(device)}`); } } @@ -744,7 +782,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { case 'Fan': case 'DIY Fan': this.debugLog(`Discovered ${device.remoteType}: ${device.deviceId}`); - this.createFan(device); + this.createIRFan(device); break; case 'Air Conditioner': case 'DIY Air Conditioner': @@ -802,7 +840,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -830,7 +868,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -862,7 +900,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -890,11 +928,11 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); - // accessory.context.FirmwareRevision = findaccessories.accessoryAttribute.softwareRevision; + // accessory.context.version = findaccessories.accessoryAttribute.softwareRevision; // create the accessory handler for the newly create accessory // this is imported from `platformAccessory.ts` new Bot(this, accessory, device); @@ -923,7 +961,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -951,7 +989,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -984,7 +1022,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1012,7 +1050,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1045,7 +1083,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1073,7 +1111,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1105,7 +1143,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1133,7 +1171,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1150,6 +1188,66 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { } } + private async createWaterDetector(device: device & devicesConfig) { + const uuid = this.api.hap.uuid.generate(`${device.deviceId}-${device.deviceType}`); + + // see if an accessory with the same uuid has already been registered and restored from + // the cached devices we stored in the `configureAccessory` method above + const existingAccessory = this.accessories.find((accessory) => accessory.UUID === uuid); + + if (existingAccessory) { + // the accessory already exists + if (await this.registerDevice(device)) { + // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: + existingAccessory.context.model = device.deviceType; + existingAccessory.context.deviceID = device.deviceId; + existingAccessory.displayName = device.configDeviceName || device.deviceName; + if (device.firmware) { + existingAccessory.context.version = device.firmware; + } + existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; + this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); + existingAccessory.context.connectionType = await this.connectionType(device); + this.api.updatePlatformAccessories([existingAccessory]); + // create the accessory handler for the restored accessory + // this is imported from `platformAccessory.ts` + new WaterDetector(this, existingAccessory, device); + this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${existingAccessory.UUID})`); + } else { + this.unregisterPlatformAccessories(existingAccessory); + } + } else if (await this.registerDevice(device)) { + // the accessory does not yet exist, so we need to create it + if (!device.external) { + this.infoLog(`Adding new accessory: ${device.deviceName} ${device.deviceType} DeviceID: ${device.deviceId}`); + } + + // create a new accessory + const accessory = new this.api.platformAccessory(device.deviceName, uuid); + + // store a copy of the device object in the `accessory.context` + // the `context` property can be used to store any data about the accessory you may need + accessory.context.device = device; + accessory.context.model = device.deviceType; + accessory.context.deviceID = device.deviceId; + if (device.firmware) { + accessory.context.version = device.firmware; + } + accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; + accessory.context.connectionType = await this.connectionType(device); + // create the accessory handler for the newly create accessory + // this is imported from `platformAccessory.ts` + new WaterDetector(this, accessory, device); + this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${accessory.UUID})`); + + // publish device externally or link the accessory to your platform + this.externalOrPlatform(device, accessory); + this.accessories.push(accessory); + } else { + this.debugLog(`Device not registered: ${device.deviceName} ${device.deviceType} DeviceID: ${device.deviceId}`); + } + } + private async createMotion(device: device & devicesConfig) { const uuid = this.api.hap.uuid.generate(`${device.deviceId}-${device.deviceType}`); @@ -1165,7 +1263,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1193,7 +1291,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1225,7 +1323,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1253,7 +1351,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1285,7 +1383,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1307,8 +1405,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { if (device.group && !device.curtain?.disable_group) { this.debugLog( 'Your Curtains are grouped, ' + - `, Secondary curtain automatically hidden. Main Curtain: ${device.deviceName}, DeviceID: ${device.deviceId}`, - ); + `, Secondary curtain automatically hidden. Main Curtain: ${device.deviceName}, DeviceID: ${device.deviceId}`); } else { if (device.master) { this.warnLog(`Main Curtain: ${device.deviceName}, DeviceID: ${device.deviceId}`); @@ -1326,7 +1423,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1358,7 +1455,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1380,8 +1477,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { if (device.group && !device.curtain?.disable_group) { this.debugLog( 'Your Curtains are grouped, ' + - `, Secondary curtain automatically hidden. Main Curtain: ${device.deviceName}, DeviceID: ${device.deviceId}`, - ); + `, Secondary curtain automatically hidden. Main Curtain: ${device.deviceName}, DeviceID: ${device.deviceId}`); } else { if (device.master) { this.warnLog(`Main Curtain: ${device.deviceName}, DeviceID: ${device.deviceId}`); @@ -1399,7 +1495,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1431,7 +1527,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1459,7 +1555,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1491,7 +1587,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1519,7 +1615,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1551,7 +1647,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1579,7 +1675,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1611,7 +1707,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1639,7 +1735,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1671,7 +1767,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1699,7 +1795,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1716,6 +1812,66 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { } } + private async createFan(device: device & devicesConfig) { + const uuid = this.api.hap.uuid.generate(`${device.deviceId}-${device.deviceType}`); + + // see if an accessory with the same uuid has already been registered and restored from + // the cached devices we stored in the `configureAccessory` method above + const existingAccessory = this.accessories.find((accessory) => accessory.UUID === uuid); + + if (existingAccessory) { + // the accessory already exists + if (await this.registerDevice(device)) { + // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: + existingAccessory.context.model = device.deviceType; + existingAccessory.context.deviceID = device.deviceId; + existingAccessory.displayName = device.configDeviceName || device.deviceName; + if (device.firmware) { + existingAccessory.context.version = device.firmware; + } + existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; + this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); + existingAccessory.context.connectionType = await this.connectionType(device); + this.api.updatePlatformAccessories([existingAccessory]); + // create the accessory handler for the restored accessory + // this is imported from `platformAccessory.ts` + new Fan(this, existingAccessory, device); + this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${existingAccessory.UUID})`); + } else { + this.unregisterPlatformAccessories(existingAccessory); + } + } else if (await this.registerDevice(device)) { + // the accessory does not yet exist, so we need to create it + if (!device.external) { + this.infoLog(`Adding new accessory: ${device.deviceName} ${device.deviceType} DeviceID: ${device.deviceId}`); + } + + // create a new accessory + const accessory = new this.api.platformAccessory(device.deviceName, uuid); + + // store a copy of the device object in the `accessory.context` + // the `context` property can be used to store any data about the accessory you may need + accessory.context.device = device; + accessory.context.model = device.deviceType; + accessory.context.deviceID = device.deviceId; + if (device.firmware) { + accessory.context.version = device.firmware; + } + accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; + accessory.context.connectionType = await this.connectionType(device); + // create the accessory handler for the newly create accessory + // this is imported from `platformAccessory.ts` + new Fan(this, accessory, device); + this.debugLog(`${device.deviceType} uuid: ${device.deviceId}-${device.deviceType}, (${accessory.UUID})`); + + // publish device externally or link the accessory to your platform + this.externalOrPlatform(device, accessory); + this.accessories.push(accessory); + } else { + this.debugLog(`Device not registered: ${device.deviceName} ${device.deviceType} DeviceID: ${device.deviceId}`); + } + } + private async createRobotVacuumCleaner(device: device & devicesConfig) { const uuid = this.api.hap.uuid.generate(`${device.deviceId}-${device.deviceType}`); @@ -1731,7 +1887,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `SwitchBot: ${device.deviceType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1759,7 +1915,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.deviceType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `SwitchBot: ${device.deviceType}`; accessory.context.connectionType = await this.connectionType(device); @@ -1788,7 +1944,9 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.model = device.remoteType; existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; - existingAccessory.context.FirmwareRevision = device.firmware; + if (device.version && !device.firmware) { + existingAccessory.context.version = device.version; + } existingAccessory.context.deviceType = `IR: ${device.remoteType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); existingAccessory.context.connectionType = device.connectionType; @@ -1812,7 +1970,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.remoteType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `IR: ${device.remoteType}`; accessory.context.connectionType = device.connectionType; @@ -1828,7 +1986,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { } } - private async createFan(device: irdevice & devicesConfig) { + private async createIRFan(device: irdevice & devicesConfig) { const uuid = this.api.hap.uuid.generate(`${device.deviceId}-${device.remoteType}`); // see if an accessory with the same uuid has already been registered and restored from @@ -1843,7 +2001,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `IR: ${device.remoteType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1851,7 +2009,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { this.api.updatePlatformAccessories([existingAccessory]); // create the accessory handler for the restored accessory // this is imported from `platformAccessory.ts` - new Fan(this, existingAccessory, device); + new IRFan(this, existingAccessory, device); this.debugLog(`${device.remoteType} uuid: ${device.deviceId}-${device.remoteType}, (${existingAccessory.UUID})`); } else { this.unregisterPlatformAccessories(existingAccessory); @@ -1871,13 +2029,13 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.remoteType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `IR: ${device.remoteType}`; accessory.context.connectionType = device.connectionType; // create the accessory handler for the newly create accessory // this is imported from `platformAccessory.ts` - new Fan(this, accessory, device); + new IRFan(this, accessory, device); this.debugLog(`${device.remoteType} uuid: ${device.deviceId}-${device.remoteType}, (${accessory.UUID})`); // publish device externally or link the accessory to your platform @@ -1903,7 +2061,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `IR: ${device.remoteType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1931,7 +2089,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.remoteType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `IR: ${device.remoteType}`; accessory.context.connectionType = device.connectionType; @@ -1963,7 +2121,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `IR: ${device.remoteType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -1991,7 +2149,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.remoteType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `IR: ${device.remoteType}`; accessory.context.connectionType = device.connectionType; @@ -2023,7 +2181,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `IR: ${device.remoteType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -2051,7 +2209,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.remoteType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `IR: ${device.remoteType}`; accessory.context.connectionType = device.connectionType; @@ -2083,7 +2241,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `IR: ${device.remoteType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -2111,7 +2269,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.remoteType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `IR: ${device.remoteType}`; accessory.context.connectionType = device.connectionType; @@ -2143,7 +2301,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `IR: ${device.remoteType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -2171,7 +2329,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.remoteType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `IR: ${device.remoteType}`; accessory.context.connectionType = device.connectionType; @@ -2203,7 +2361,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `IR: ${device.remoteType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -2231,7 +2389,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.remoteType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `IR: ${device.remoteType}`; accessory.context.connectionType = device.connectionType; @@ -2263,7 +2421,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { existingAccessory.context.deviceID = device.deviceId; existingAccessory.displayName = device.configDeviceName || device.deviceName; if (device.firmware) { - existingAccessory.context.FirmwareRevision = device.firmware; + existingAccessory.context.version = device.firmware; } existingAccessory.context.deviceType = `IR: ${device.remoteType}`; this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.deviceId}`); @@ -2291,7 +2449,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { accessory.context.model = device.remoteType; accessory.context.deviceID = device.deviceId; if (device.firmware) { - accessory.context.FirmwareRevision = device.firmware; + accessory.context.version = device.firmware; } accessory.context.deviceType = `IR: ${device.remoteType}`; accessory.context.connectionType = device.connectionType; @@ -2312,8 +2470,8 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { switch (device.deviceType) { case 'Curtain': case 'Curtain3': - this.debugWarnLog(`deviceName: ${device.deviceName} deviceId: ${device.deviceId}, curtainDevicesIds: ${device.curtainDevicesIds}, master: ` + - `${device.master}, group: ${device.group}, disable_group: ${device.curtain?.disable_group}, connectionType: ${device.connectionType}`); + this.debugWarnLog(`deviceName: ${device.deviceName} deviceId: ${device.deviceId}, curtainDevicesIds: ${device.curtainDevicesIds}, master: ` + + `${device.master}, group: ${device.group}, disable_group: ${device.curtain?.disable_group}, connectionType: ${device.connectionType}`); break; default: this.debugWarnLog(`deviceName: ${device.deviceName} deviceId: ${device.deviceId}, blindTiltDevicesIds: ${device.blindTiltDevicesIds}, master:` @@ -2324,41 +2482,32 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { if (device.master && device.group) { // OpenAPI: Master Curtains/Blind Tilt in Group registerCurtain = true; - this.debugLog( - `deviceName: ${device.deviceName} [${device.deviceType} Config] device.master: ${device.master}, device.group: ${device.group}` + - ` connectionType; ${device.connectionType}`, - ); + this.debugLog(`deviceName: ${device.deviceName} [${device.deviceType} Config] device.master: ${device.master}, device.group: ${device.group}` + + ` connectionType; ${device.connectionType}`); this.debugWarnLog(`Device: ${device.deviceName} registerCurtains: ${registerCurtain}`); } else if (!device.master && device.curtain?.disable_group) { //!device.group && device.connectionType === 'BLE' // OpenAPI: Non-Master Curtains/Blind Tilts that has Disable Grouping Checked registerCurtain = true; - this.debugLog( - `deviceName: ${device.deviceName} [${device.deviceType} Config] device.master: ${device.master}, disable_group: ` + - `${device.curtain?.disable_group}, connectionType; ${device.connectionType}`, - ); + this.debugLog(`deviceName: ${device.deviceName} [${device.deviceType} Config] device.master: ${device.master}, disable_group: ` + + `${device.curtain?.disable_group}, connectionType; ${device.connectionType}`); this.debugWarnLog(`Device: ${device.deviceName} registerCurtains: ${registerCurtain}`); } else if (device.master && !device.group) { // OpenAPI: Master Curtains/Blind Tilts not in Group registerCurtain = true; - this.debugLog( - `deviceName: ${device.deviceName} [${device.deviceType} Config] device.master: ${device.master}, device.group: ${device.group}` + - ` connectionType; ${device.connectionType}`, - ); + this.debugLog(`deviceName: ${device.deviceName} [${device.deviceType} Config] device.master: ${device.master}, device.group: ${device.group}` + + ` connectionType; ${device.connectionType}`); this.debugWarnLog(`Device: ${device.deviceName} registerCurtains: ${registerCurtain}`); } else if (device.connectionType === 'BLE') { // BLE: Curtains/Blind Tilt registerCurtain = true; - this.debugLog( - `deviceName: ${device.deviceName} [${device.deviceType} Config] connectionType: ${device.connectionType}, ` + ` group: ${device.group}`, - ); + this.debugLog(`deviceName: ${device.deviceName} [${device.deviceType} Config] connectionType: ${device.connectionType}, ` + + ` group: ${device.group}`); this.debugWarnLog(`Device: ${device.deviceName} registerCurtains: ${registerCurtain}`); } else { registerCurtain = false; - this.debugErrorLog( - `deviceName: ${device.deviceName} [${device.deviceType} Config] disable_group: ${device.curtain?.disable_group},` + - ` device.master: ${device.master}, device.group: ${device.group}`, - ); + this.debugErrorLog(`deviceName: ${device.deviceName} [${device.deviceType} Config] disable_group: ${device.curtain?.disable_group},` + + ` device.master: ${device.master}, device.group: ${device.group}`); this.debugWarnLog(`Device: ${device.deviceName} registerCurtains: ${registerCurtain}, device.connectionType: ${device.connectionType}`); } return registerCurtain; @@ -2449,10 +2598,8 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { this.debugErrorLog(`Device: ${device.deviceName} hide_device: ${device.hide_device}, will not display in HomeKit`); } else { registerDevice = false; - this.debugErrorLog( - `Device: ${device.deviceName} connectionType: ${device.connectionType}, hide_device: ` + - `${device.hide_device}, will not display in HomeKit`, - ); + this.debugErrorLog(`Device: ${device.deviceName} connectionType: ${device.connectionType}, hide_device: ` + + `${device.hide_device}, will not display in HomeKit`); } return registerDevice; } @@ -2495,16 +2642,14 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { async statusCode(statusCode: number): Promise { switch (statusCode) { case 151: - this.errorLog( - `Command not supported by this device type, statusCode: ${statusCode}, Submit Feature Request Here: ` + - 'https://tinyurl.com/SwitchBotFeatureRequest', - ); + this.errorLog(`Command not supported by this device type, statusCode: ${statusCode}, Submit Feature Request Here: ` + + 'https://tinyurl.com/SwitchBotFeatureRequest'); break; case 152: this.errorLog(`Device not found, statusCode: ${statusCode}`); break; case 160: - this.errorLog(`Command is not supported, statusCode: ${statusCode}, Submit Bugs Here: ' + 'https://tinyurl.com/SwitchBotBug`); + this.errorLog(`Command is not supported, statusCode: ${statusCode}, Submit Bugs Here: https://tinyurl.com/SwitchBotBug`); break; case 161: this.errorLog(`Device is offline, statusCode: ${statusCode}`); @@ -2561,6 +2706,30 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { } } + async retryRequest(deviceMaxRetries: number, deviceDelayBetweenRetries: number, url: string | URL | UrlObject, + options?: { dispatcher?: Dispatcher } & Omit & Partial>): Promise<{ body: any; statusCode: number }> { + let retryCount = 0; + const maxRetries = deviceMaxRetries; + const delayBetweenRetries = deviceDelayBetweenRetries; + while (retryCount < maxRetries) { + try { + const { body, statusCode } = await request(url, options); + if (statusCode === 200 || statusCode === 100) { + return { body, statusCode }; + } else { + this.debugLog(`Received status code: ${statusCode}`); + } + } catch (error: any) { + this.errorLog(`Error making request: ${error.message}`); + } + retryCount++; + this.debugLog(`Retry attempt ${retryCount} of ${maxRetries}`); + await sleep(delayBetweenRetries); + } + return { body: null, statusCode: -1 }; + } + // BLE Connection async connectBLE() { let switchbot: any; @@ -2585,7 +2754,7 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { this.version = json.version; } - async platformConfigOptions() { + async getPlatformConfigSettings() { const platformConfig: SwitchBotPlatformConfig['options'] = {}; if (this.config.options) { if (this.config.options.logging) { @@ -2594,9 +2763,28 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { if (this.config.options.refreshRate) { platformConfig.refreshRate = this.config.options.refreshRate; } + if (this.config.options.updateRate) { + platformConfig.updateRate = this.config.options.updateRate; + } if (this.config.options.pushRate) { platformConfig.pushRate = this.config.options.pushRate; } + if (this.config.options.maxRetries) { + this.maxRetries = this.config.options.maxRetries; + platformConfig.maxRetries = this.config.options.maxRetries; + } else { + this.maxRetries = 3; + this.debugWarnLog('Using Default Max Retries'); + platformConfig.maxRetries = this.maxRetries; + } + if (this.config.options.delayBetweenRetries) { + this.delayBetweenRetries = this.config.options.delayBetweenRetries * 1000; + platformConfig.delayBetweenRetries = this.config.options.delayBetweenRetries; + } else { + this.delayBetweenRetries = 3000; + this.debugWarnLog('Using Default Delay Between Retries'); + platformConfig.delayBetweenRetries = this.delayBetweenRetries / 1000; + } if (Object.entries(platformConfig).length !== 0) { this.debugLog(`Platform Config: ${JSON.stringify(platformConfig)}`); } @@ -2604,10 +2792,10 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { } } - async platformLogs() { + async getPlatformLogSettings() { this.debugMode = process.argv.includes('-D') || process.argv.includes('--debug'); if (this.config.options?.logging === 'debug' || this.config.options?.logging === 'standard' || this.config.options?.logging === 'none') { - this.platformLogging = this.config.options!.logging; + this.platformLogging = this.config.options.logging; this.debugWarnLog(`Using Config Logging: ${this.platformLogging}`); } else if (this.debugMode) { this.platformLogging = 'debugMode'; @@ -2628,6 +2816,20 @@ export class SwitchBotPlatform implements DynamicPlatformPlugin { } } + successLog(...log: any[]): void { + if (this.enablingPlatformLogging()) { + this.log.success(String(...log)); + } + } + + debugSuccessLog(...log: any[]): void { + if (this.enablingPlatformLogging()) { + if (this.platformLogging?.includes('debug')) { + this.log.success('[DEBUG]', String(...log)); + } + } + } + warnLog(...log: any[]): void { if (this.enablingPlatformLogging()) { this.log.warn(String(...log)); diff --git a/src/settings.ts b/src/settings.ts index 59f89635..4226857b 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,10 +1,9 @@ -/* Copyright(C) 2017-2023, donavanbecker (https://github.com/donavanbecker). All rights reserved. +/* Copyright(C) 2017-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. * * settings.ts: @switchbot/homebridge-switchbot platform class. */ -/* eslint-disable max-len */ -import { MacAddress, PlatformConfig } from 'homebridge'; -import { IClientOptions } from 'async-mqtt'; +import type { IClientOptions } from 'async-mqtt'; +import type { MacAddress, PlatformConfig } from 'homebridge'; /** * This is the name of the platform that users will use to register the plugin in the Homebridge config.json */ @@ -20,6 +19,26 @@ export const PLUGIN_NAME = '@switchbot/homebridge-switchbot'; */ export const Devices = 'https://api.switch-bot.com/v1.1/devices'; +/** + * This is the updateWebhook url used to access SwitchBot API + */ +export const setupWebhook = 'https://api.switch-bot.com/v1.1/webhook/setupWebhook'; + +/** + * This is the updateWebhook url used to access SwitchBot API + */ +export const queryWebhook = 'https://api.switch-bot.com/v1.1/webhook/queryWebhook'; + +/** + * This is the updateWebhook url used to access SwitchBot API + */ +export const updateWebhook = 'https://api.switch-bot.com/v1.1/webhook/updateWebhook'; + +/** + * This is the deleteWebhook url used to access SwitchBot API + */ +export const deleteWebhook = 'https://api.switch-bot.com/v1.1/webhook/deleteWebhook'; + //Config export interface SwitchBotPlatformConfig extends PlatformConfig { credentials?: credentials; @@ -35,10 +54,13 @@ export type credentials = { export type options = { refreshRate?: number; + updateRate?: number; pushRate?: number; + maxRetries?: number; + delayBetweenRetries?: number; logging?: string; - devices?: Array; - irdevices?: Array; + devices?: devicesConfig[]; + irdevices?: irDevicesConfig[]; webhookURL?: string; mqttURL?: string; mqttOptions?: IClientOptions; @@ -46,11 +68,16 @@ export type options = { }; export interface devicesConfig extends device { + bleMac?: string; + model?: string; + bleModel?: string; configDeviceType: string; configDeviceName?: string; deviceId: string; external?: boolean; refreshRate?: number; + updateRate?: number; + pushRate?: number; firmware?: string; logging?: string; connectionType?: string; @@ -59,6 +86,8 @@ export interface devicesConfig extends device { hide_device?: boolean; offline?: boolean; maxRetry?: number; + maxRetries?: number; + delayBetweenRetries?: number; disableCaching?: boolean; mqttURL?: string; mqttOptions?: IClientOptions; @@ -67,11 +96,13 @@ export interface devicesConfig extends device { webhook?: boolean; bot?: bot; meter?: meter; + iosensor?: iosensor; humidifier?: humidifier; curtain?: curtain; blindTilt?: blindTilt; contact?: contact; motion?: motion; + waterdetector?: waterdetector; colorbulb?: colorbulb; striplight?: striplight; ceilinglight?: ceilinglight; @@ -82,6 +113,13 @@ export interface devicesConfig extends device { export type meter = { hide_temperature?: boolean; + convertUnitTo?: string; + hide_humidity?: boolean; +}; + +export type iosensor = { + hide_temperature?: boolean; + convertUnitTo?: string; hide_humidity?: boolean; }; @@ -104,7 +142,6 @@ export type curtain = { hide_lightsensor?: boolean; set_minLux?: number; set_maxLux?: number; - updateRate?: number; set_max?: number; set_min?: number; set_minStep?: number; @@ -117,7 +154,6 @@ export type blindTilt = { hide_lightsensor?: boolean; set_minLux?: number; set_maxLux?: number; - updateRate?: number; set_max?: number; set_min?: number; set_minStep?: number; @@ -138,6 +174,10 @@ export type motion = { set_maxLux?: number; }; +export type waterdetector = { + hide_leak?: boolean; +}; + export type colorbulb = { set_minStep?: number; adaptiveLightingShift?: number; @@ -160,6 +200,7 @@ export type lock = { export type hub = { hide_temperature?: boolean; + convertUnitTo?: string; hide_humidity?: boolean; hide_lightsensor?: boolean; }; @@ -225,7 +266,7 @@ export type body = { //a list of physical devices. export type deviceList = { - device: Array; + device: device[]; }; export type device = { @@ -240,9 +281,9 @@ export type device = { //device's parent Hub ID. hubDeviceId: string; //only available for Curtain devices. a list of Curtain device IDs such that the Curtain devices are being paired or grouped. - curtainDevicesIds?: Array; + curtainDevicesIds?: string[]; //only available for Blind Titl devices. a list of Blind Tilt device IDs such that the Blind Tilt devices are being paired or grouped. - blindTiltDevicesIds?: Array; + blindTiltDevicesIds?: string[]; //only available for Curtain/Lock devices. determines if the open position and the close position of a device have been properly calibrated or not calibrate?: boolean; //only available for Curtain devices. determines if a Curtain is paired with or grouped with another Curtain or not. @@ -256,14 +297,28 @@ export type device = { //the current position, 0-100 slidePosition?: string; //the version of the device - version?: number; - //BLE Mac Address - bleMac?: string; + version?: string; + // Fan Mode: direct mode: direct; natural mode: "natural"; sleep mode: "sleep"; ultra quiet mode: "baby" + mode: string; + //the current battery level + battery: number; + //ON/OFF state + power: string; + //set nightlight status. turn off: off; mode 1: 1; mode 2: 2 + nightStatus: number; + //set horizontal oscillation. turn on: on; turn off: off + oscillation: string; + //set vertical oscillation. turn on: on; turn off: off + verticalOscillation: string; + //battery charge status. charging or uncharged + chargingStatus: string; + //fan speed. 1~100 + fanSpeed: number; }; //a list of virtual infrared remote devices. export type infraredRemoteList = { - device: Array; + device: irdevice[]; }; export type irdevice = { @@ -271,48 +326,62 @@ export type irdevice = { deviceName: string; //device name remoteType: string; //device type hubDeviceId: string; //remote device's parent Hub ID + model: string; //device model }; export type deviceStatus = { + statusCode: number; + message: string; + body: deviceStatusBody; +}; + +export type deviceStatusBody = { //v1.1 of API - deviceId: string; //device ID. (Used by the following deviceTypes: Bot, Curtain, Meter, Meter Plus, Lock, Keypad, Keypad Touch, Motion Sensor, Contact Sensor, Ceiling Light, Ceiling Light Pro, Plug Mini (US), Plug Mini (JP), Strip Light, Color Bulb, Robot Vacuum Cleaner S1, Robot Vacuum Cleaner S1 Plus, Humidifier, Blind Tilt) - deviceType: string; //device type. (Used by the following deviceTypes: Bot, Curtain, Meter, Meter Plus, Lock, Keypad, Keypad Touch, Motion Sensor, Contact Sensor, Ceiling Light, Ceiling Light Pro, Plug Mini (US), Plug Mini (JP), Strip Light, Color Bulb, Robot Vacuum Cleaner S1, Robot Vacuum Cleaner S1 Plus, Humidifier, Blind Tilt) - hubDeviceId: string; //device's parent Hub ID. 000000000000 when the device itself is a Hub or it is connected through Wi-Fi. (Used by the following deviceTypes: Bot, Curtain, Meter, Meter Plus, Lock, Keypad, Keypad Touch, Motion Sensor, Contact Sensor, Ceiling Light, Ceiling Light Pro, Plug Mini (JP), Strip Light, Color Bulb, Robot Vacuum Cleaner S1, Robot Vacuum Cleaner S1 Plus, Humidifier, Blind Tilt) - power?: string; //ON/OFF state. (Used by the following deviceTypes: Bot, Ceiling Light, Ceiling Light Pro, PLug, Plug Mini (US), Plug Mini (JP), Strip Light, Color Bulb, Humidifier) - calibrate?: boolean; //determines if device has been calibrated or not. (Used by the following deviceTypes: Curtain, Lock, Blind Tilt) - group?: boolean; //determines if a device is paired with or grouped with another device or not. (Used by the following deviceTypes: Curtain, Blind Tilt) - moving?: boolean; //determines if a device is moving or not. (Used by the following deviceTypes: Curtain, Blind Tilt) - slidePosition?: number; //the current position (0-100) the percentage of the distance between the calibrated open position and closed position. (Used by the following deviceTypes: Curtain, Blind Tilt) - temperature?: number; //temperature in celsius (Used by the following deviceTypes: Meter, Meter Plus, Humidifier, IOSensor) - humidity?: number; //humidity percentage. (Used by the following deviceTypes: Meter, Meter Plus, Humidifier, IOSensor) - lockState?: string; //determines if locked or not. (Used by the following deviceTypes: Lock) - doorState?: string; //determines if the door is closed or not. (Used by the following deviceTypes: Lock) - moveDetected?: boolean; //determines if motion is detected. (Used by the following deviceTypes: Motion Sensor, Contact Sensor) - brightness?: string | number; //the ambient brightness picked up by the sensor. bright or dim. (Used by the following deviceTypes: Motion Sensor, Contact Sensor) | the brightness value, range from 1 to 100. (Used by the following deviceTypes: Ceiling Light, Ceiling Light Pro, Strip Light, Color Bulb) - openState?: string; //the open state of the sensor. open, close, or timeOutNotClose. (Used by the following deviceTypes: Contact Sensor) - colorTemperature?: number; //the color temperature value, range from 2700 to 6500. (Used by the following deviceTypes: Ceiling Light, Ceiling Light Pro, Color Bulb) - voltage?: number; //the voltage of the device, measured in Volt. (Used by the following deviceTypes: Plug Mini (US), Plug Mini (JP)) - weight?: number; //the power consumed in a day, measured in Watts. (Used by the following deviceTypes: Plug Mini (US), Plug Mini (JP)) - electricityOfDay?: number; //the duration that the device has been used during a day, measured in minutes. (Used by the following deviceTypes: Plug Mini (US), Plug Mini (JP)) - electricCurrent?: number; //the current of the device at the moment, measured in Amp. (Used by the following deviceTypes: Plug Mini (US), Plug Mini (JP)) - color?: string; //the color value, RGB "255:255:255". (Used by the following deviceTypes: Strip Light, Color Bulb) - workingStatus?: string; //the working status of the device. StandBy, Clearing, Paused, GotoChargeBase, Charging, ChargeDone, Dormant, InTrouble, InRemoteControl, or InDustCollecting. (Used by the following deviceTypes: Robot Vacuum Cleaner S1, Robot Vacuum Cleaner S1 Plus) - onlineStatus?: string; //the connection status of the device. online or offline. (Used by the following deviceTypes: Robot Vacuum Cleaner S1, Robot Vacuum Cleaner S1 Plus) - battery?: number; //the current battery level. (Used by the following deviceTypes: Robot Vacuum Cleaner S1, Robot Vacuum Cleaner S1 Plus, Blind Tilt, IOSensor) - deviceName?: string; //device name. (Used by the following deviceTypes: Robot Vacuum Cleaner S1 Plus) - nebulizationEfficiency?: number; //atomization efficiency percentage. (Used by the following deviceTypes: Humidifier) - auto?: boolean; //determines if a Humidifier is in Auto Mode or not. (Used by the following deviceTypes: Humidifier) - childLock?: boolean; //determines if a Humidifier's safety lock is on or not. (Used by the following deviceTypes: Humidifier) - sound?: boolean; //determines if a Humidifier is muted or not. (Used by the following deviceTypes: Humidifier) - lackWater?: boolean; //determines if the water tank is empty or not. (Used by the following deviceTypes: Humidifier) - version?: number; //the version of the device. (Used by the following deviceTypes: Blind Tilt, Meter, MeterPlus, IOSensor) - direction?: string; //the opening direction of a Blind Tilt device. (Used by the following deviceTypes: Blind Tilt) - runStatus?: string; //'static' when not moving. (Used by the following deviceTypes: Blind Tilt) - mode?: number; //available for devices. the fan mode. (Used by the following deviceTypes: Smart Fan) - speed?: number; //the fan speed. (Used by the following deviceTypes: Smart Fan) - shaking?: boolean; //determines if the fan is swinging or not. (Used by the following deviceTypes: Smart Fan) - shakeCenter?: string; //the fan's swing direction. (Used by the following deviceTypes: Smart Fan) - shakeRange?: string; //the fan's swing range, 0~120°. (Used by the following deviceTypes: Smart Fan) + deviceId: string; + deviceType: string; + hubDeviceId: string; + power?: string; + calibrate?: boolean; + group?: boolean; + moving?: boolean; + slidePosition?: number; + temperature?: number; + humidity?: number; + lockState?: string; + doorState?: string; + moveDetected?: boolean; + brightness?: string | number; + openState?: string; + colorTemperature?: number; + voltage?: number; + weight?: number; + electricityOfDay?: number; + electricCurrent?: number; + color?: string; + workingStatus?: string; + onlineStatus?: string; + battery?: number; + deviceName?: string; + nebulizationEfficiency?: number; + auto?: boolean; + childLock?: boolean; + sound?: boolean; + lackWater?: boolean; + version?: number; + direction?: string; + runStatus?: string; + mode?: number | string; + speed?: number; + shaking?: boolean; + shakeCenter?: string; + shakeRange?: string; + status?: number; + lightLevel?: number; + nightStatus: number; + oscillation: string; + verticalOscillation: string; + chargingStatus: string; + fanSpeed: number; }; export type ad = { @@ -349,7 +418,7 @@ export type serviceData = { battery?: number; //Humidifier's humidity level percentage percentage?: boolean | string; - //Humidifier's humidity level percentage + //Humidifier's state onState?: boolean; //Humidifier's AutoMode autoMode?: boolean; @@ -400,557 +469,3 @@ export type switchbot = { wait: (arg0: number) => any; }; -export function rgb2hs(r: any, g: any, b: any) { - /* - Credit: - https://github.com/WickyNilliams/pure-color - */ - r = parseInt(r); - g = parseInt(g); - b = parseInt(b); - const min = Math.min(r, g, b); - const max = Math.max(r, g, b); - const delta = max - min; - let h; - let s; - if (max === 0) { - s = 0; - } else { - s = (delta / max) * 100; - } - if (max === min) { - h = 0; - } else if (r === max) { - h = (g - b) / delta; - } else if (g === max) { - h = 2 + (b - r) / delta; - } else if (b === max) { - h = 4 + (r - g) / delta; - } - h = Math.min(h * 60, 360); - - if (h < 0) { - h += 360; - } - return [Math.round(h), Math.round(s)]; -} - -export function hs2rgb(h: any, s: any) { - /* - Credit: - https://github.com/WickyNilliams/pure-color - */ - h = parseInt(h) / 60; - s = parseInt(s) / 100; - const f = h - Math.floor(h); - const p = 255 * (1 - s); - const q = 255 * (1 - s * f); - const t = 255 * (1 - s * (1 - f)); - let rgb; - switch (Math.floor(h) % 6) { - case 0: - rgb = [255, t, p]; - break; - case 1: - rgb = [q, 255, p]; - break; - case 2: - rgb = [p, 255, t]; - break; - case 3: - rgb = [p, q, 255]; - break; - case 4: - rgb = [t, p, 255]; - break; - case 5: - rgb = [255, p, q]; - break; - } - if (rgb[0] === 255) { - rgb[1] *= 0.8; - rgb[2] *= 0.8; - if (rgb[1] <= 25 && rgb[2] <= 25) { - rgb[1] = 0; - rgb[2] = 0; - } - } - return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2])]; -} - -export function k2rgb(k: number) { - // Set kelvin to nearest 100, between 2000 and 7100 - k = Math.round(k / 100) * 100; - k = Math.max(Math.min(k, 7100), 2000); - - // k should now appear in our table of kelvin to rgb - const values = { - 2000: [255, 141, 11], - 2100: [255, 146, 29], - 2200: [255, 147, 44], - 2300: [255, 152, 54], - 2400: [255, 157, 63], - 2500: [255, 166, 69], - 2600: [255, 170, 77], - 2700: [255, 174, 84], - 2800: [255, 173, 94], - 2900: [255, 177, 101], - 3000: [255, 180, 107], - 3100: [255, 189, 111], - 3200: [255, 187, 120], - 3300: [255, 195, 124], - 3400: [255, 198, 130], - 3500: [255, 201, 135], - 3600: [255, 203, 141], - 3700: [255, 206, 146], - 3800: [255, 204, 153], - 3900: [255, 206, 159], - 4000: [255, 213, 161], - 4100: [255, 215, 166], - 4200: [255, 217, 171], - 4300: [255, 219, 175], - 4400: [255, 221, 180], - 4500: [255, 223, 184], - 4600: [255, 225, 188], - 4700: [255, 226, 192], - 4800: [255, 228, 196], - 4900: [255, 229, 200], - 5000: [255, 231, 204], - 5100: [255, 230, 210], - 5200: [255, 234, 211], - 5300: [255, 235, 215], - 5400: [255, 237, 218], - 5500: [255, 236, 224], - 5700: [255, 240, 228], - 5800: [255, 241, 231], - 5900: [255, 243, 234], - 6000: [255, 244, 237], - 6100: [255, 245, 240], - 6200: [255, 246, 243], - 6300: [255, 247, 247], - 6400: [255, 248, 251], - 6500: [255, 249, 253], - 6600: [254, 249, 255], - 6700: [252, 247, 255], - 6800: [249, 246, 255], - 6900: [247, 245, 255], - 7000: [245, 243, 255], - 7100: [243, 242, 255], - }; - - // Return the value - return values[k]; -} - -export function m2hs(m) { - /* - Credit: - https://github.com/homebridge/HAP-NodeJS - */ - const table = { - 100: [19, 222.1], - 101: [18.7, 222.2], - 102: [18.4, 222.3], - 103: [18.2, 222.3], - 104: [17.9, 222.4], - 105: [17.6, 222.5], - 106: [17.3, 222.7], - 107: [17, 222.8], - 108: [16.7, 222.9], - 109: [16.4, 223], - 110: [16.1, 223.2], - 111: [15.8, 223.3], - 112: [15.4, 223.4], - 113: [15.2, 223.6], - 114: [14.9, 223.8], - 115: [14.7, 223.9], - 116: [14.3, 224.1], - 117: [14.1, 224.2], - 118: [13.8, 224.4], - 119: [13.5, 224.6], - 120: [13.2, 224.8], - 121: [12.9, 225], - 122: [12.5, 225.3], - 123: [12.2, 225.6], - 124: [11.8, 225.9], - 125: [11.4, 226.3], - 126: [11.1, 226.7], - 127: [10.7, 227.1], - 128: [10.3, 227.6], - 129: [9.9, 228], - 130: [9.6, 228.5], - 131: [9.3, 229.1], - 132: [8.9, 229.6], - 133: [8.5, 230.2], - 134: [8.2, 230.9], - 135: [7.8, 231.6], - 136: [7.5, 232.5], - 137: [7.1, 233.5], - 138: [6.7, 234.6], - 139: [6.3, 235.8], - 140: [6, 237.1], - 141: [5.6, 238.9], - 142: [5.2, 240.9], - 143: [5, 242.9], - 144: [4.8, 244.9], - 145: [4.6, 246.9], - 146: [4.4, 249.3], - 147: [4.3, 251.9], - 148: [4.1, 254.9], - 149: [3.9, 258], - 150: [3.7, 261.8], - 151: [3.4, 265.9], - 152: [3.2, 271], - 153: [3, 276.4], - 154: [2.8, 283.6], - 155: [2.6, 290.4], - 156: [2.3, 295.3], - 157: [2.1, 300], - 158: [1.9, 300], - 159: [1.6, 300], - 160: [1.4, 195.8], - 161: [1.2, 84.3], - 162: [1.3, 58.2], - 163: [1.5, 55.9], - 164: [1.7, 53.2], - 165: [1.9, 50.2], - 166: [2.1, 47.1], - 167: [2.4, 44.5], - 168: [2.6, 42.6], - 169: [2.9, 40.9], - 170: [3.1, 39.5], - 171: [3.4, 38.3], - 172: [3.7, 37.3], - 173: [3.9, 36.5], - 174: [4.2, 35.7], - 175: [4.4, 35.1], - 176: [4.6, 34.5], - 177: [4.9, 34], - 178: [5.1, 33.5], - 179: [5.3, 33], - 180: [5.6, 32.7], - 181: [5.8, 32.3], - 182: [6, 32], - 183: [6.3, 31.7], - 184: [6.5, 31.4], - 185: [6.7, 31.2], - 186: [7, 30.9], - 187: [7.2, 30.7], - 188: [7.4, 30.5], - 189: [7.6, 30.3], - 190: [7.9, 30.1], - 191: [8.1, 29.9], - 192: [8.4, 29.7], - 193: [8.6, 29.6], - 194: [8.9, 29.5], - 195: [9.1, 29.3], - 196: [9.4, 29.2], - 197: [9.6, 29.1], - 198: [9.8, 29], - 199: [10, 28.9], - 200: [10.2, 28.7], - 201: [10.5, 28.7], - 202: [10.7, 28.6], - 203: [11, 28.5], - 204: [11.2, 28.4], - 205: [11.4, 28.3], - 206: [11.6, 28.3], - 207: [11.8, 28.2], - 208: [12.1, 28.1], - 209: [12.3, 28.1], - 210: [12.5, 28], - 211: [12.7, 28], - 212: [12.9, 27.9], - 213: [13.2, 27.8], - 214: [13.4, 27.8], - 215: [13.6, 27.7], - 216: [13.8, 27.7], - 217: [14, 27.7], - 218: [14.3, 27.6], - 219: [14.5, 27.6], - 220: [14.7, 27.5], - 221: [14.9, 27.5], - 222: [15.1, 27.5], - 223: [15.3, 27.4], - 224: [15.5, 27.4], - 225: [15.8, 27.4], - 226: [16, 27.3], - 227: [16.2, 27.3], - 228: [16.4, 27.3], - 229: [16.6, 27.3], - 230: [16.8, 27.2], - 231: [17, 27.2], - 232: [17.2, 27.2], - 233: [17.4, 27.2], - 234: [17.6, 27.2], - 235: [17.8, 27.1], - 236: [18, 27.1], - 237: [18.2, 27.1], - 238: [18.4, 27.1], - 239: [18.7, 27.1], - 240: [18.8, 27], - 241: [19, 27], - 242: [19.2, 27], - 243: [19.4, 27], - 244: [19.6, 27], - 245: [19.8, 27], - 246: [20, 27], - 247: [20.3, 26.9], - 248: [20.5, 26.9], - 249: [20.6, 26.9], - 250: [20.8, 26.9], - 251: [21, 26.9], - 252: [21.3, 26.9], - 253: [21.5, 26.9], - 254: [21.6, 26.9], - 255: [21.8, 26.8], - 256: [22, 26.8], - 257: [22.2, 26.8], - 258: [22.4, 26.8], - 259: [22.6, 26.8], - 260: [22.8, 26.8], - 261: [23, 26.8], - 262: [23.2, 26.8], - 263: [23.4, 26.8], - 264: [23.6, 26.8], - 265: [23.8, 26.8], - 266: [24, 26.8], - 267: [24.1, 26.8], - 268: [24.3, 26.8], - 269: [24.5, 26.8], - 270: [24.7, 26.8], - 271: [24.8, 26.8], - 272: [25.1, 26.7], - 273: [25.3, 26.7], - 274: [25.4, 26.7], - 275: [25.6, 26.7], - 276: [25.8, 26.7], - 277: [26, 26.7], - 278: [26.1, 26.7], - 279: [26.3, 26.7], - 280: [26.5, 26.7], - 281: [26.7, 26.7], - 282: [26.9, 26.7], - 283: [27.1, 26.7], - 284: [27.3, 26.7], - 285: [27.5, 26.7], - 286: [27.7, 26.7], - 287: [27.8, 26.7], - 288: [28, 26.7], - 289: [28.2, 26.7], - 290: [28.4, 26.7], - 291: [28.6, 26.7], - 292: [28.8, 26.7], - 293: [28.9, 26.7], - 294: [29.1, 26.7], - 295: [29.3, 26.7], - 296: [29.5, 26.7], - 297: [29.6, 26.7], - 298: [29.8, 26.7], - 299: [30, 26.7], - 300: [30.2, 26.7], - 301: [30.4, 26.7], - 302: [30.5, 26.7], - 303: [30.7, 26.7], - 304: [30.9, 26.7], - 305: [31.1, 26.7], - 306: [31.2, 26.7], - 307: [31.4, 26.7], - 308: [31.6, 26.7], - 309: [31.8, 26.8], - 310: [31.9, 26.8], - 311: [32.1, 26.8], - 312: [32.3, 26.8], - 313: [32.5, 26.8], - 314: [32.6, 26.8], - 315: [32.8, 26.8], - 316: [33, 26.8], - 317: [33.2, 26.8], - 318: [33.3, 26.8], - 319: [33.5, 26.8], - 320: [33.7, 26.8], - 321: [33.8, 26.8], - 322: [34, 26.8], - 323: [34.2, 26.8], - 324: [34.4, 26.8], - 325: [34.5, 26.8], - 326: [34.7, 26.8], - 327: [34.9, 26.8], - 328: [35.1, 26.8], - 329: [35.2, 26.8], - 330: [35.4, 26.8], - 331: [35.5, 26.8], - 332: [35.7, 26.8], - 333: [35.9, 26.8], - 334: [36.1, 26.8], - 335: [36.3, 26.9], - 336: [36.5, 26.9], - 337: [36.7, 26.9], - 338: [36.9, 26.9], - 339: [37.1, 26.9], - 340: [37.2, 26.9], - 341: [37.4, 26.9], - 342: [37.5, 26.9], - 343: [37.7, 26.9], - 344: [37.9, 26.9], - 345: [38.1, 26.9], - 346: [38.3, 26.9], - 347: [38.5, 26.9], - 348: [38.7, 26.9], - 349: [38.9, 26.9], - 350: [39, 26.9], - 351: [39.2, 26.9], - 352: [39.3, 27], - 353: [39.5, 27], - 354: [39.7, 27], - 355: [39.9, 27], - 356: [40.1, 27], - 357: [40.2, 27], - 358: [40.4, 27], - 359: [40.6, 27], - 360: [40.8, 27], - 361: [40.9, 27], - 362: [41.1, 27], - 363: [41.2, 27], - 364: [41.4, 27], - 365: [41.6, 27], - 366: [41.8, 27], - 367: [42, 27], - 368: [42.1, 27.1], - 369: [42.3, 27.1], - 370: [42.4, 27.1], - 371: [42.6, 27.1], - 372: [42.8, 27.1], - 373: [43, 27.1], - 374: [43.1, 27.1], - 375: [43.2, 27.1], - 376: [43.4, 27.1], - 377: [43.6, 27.1], - 378: [43.8, 27.1], - 379: [43.9, 27.1], - 380: [44.1, 27.1], - 381: [44.3, 27.2], - 382: [44.4, 27.2], - 383: [44.6, 27.2], - 384: [44.7, 27.2], - 385: [44.9, 27.2], - 386: [45.1, 27.2], - 387: [45.3, 27.2], - 388: [45.5, 27.2], - 389: [45.6, 27.2], - 390: [45.8, 27.2], - 391: [46, 27.2], - 392: [46.2, 27.2], - 393: [46.4, 27.3], - 394: [46.5, 27.3], - 395: [46.7, 27.3], - 396: [46.9, 27.3], - 397: [47.1, 27.3], - 398: [47.2, 27.3], - 399: [47.4, 27.3], - 400: [47.6, 27.3], - 401: [47.7, 27.3], - 402: [47.9, 27.3], - 403: [48.1, 27.3], - 404: [48.3, 27.3], - 405: [48.5, 27.4], - 406: [48.7, 27.4], - 407: [48.8, 27.4], - 408: [49, 27.4], - 409: [49.2, 27.4], - 410: [49.4, 27.4], - 411: [49.6, 27.4], - 412: [49.7, 27.4], - 413: [49.9, 27.4], - 414: [50.1, 27.4], - 415: [50.2, 27.4], - 416: [50.4, 27.4], - 417: [50.6, 27.5], - 418: [50.7, 27.5], - 419: [50.9, 27.5], - 420: [51.1, 27.5], - 421: [51.2, 27.5], - 422: [51.4, 27.5], - 423: [51.6, 27.5], - 424: [51.7, 27.5], - 425: [51.9, 27.5], - 426: [52.1, 27.5], - 427: [51.2, 27.6], - 428: [52.4, 27.6], - 429: [52.5, 27.6], - 430: [52.7, 27.6], - 431: [52.9, 27.6], - 432: [53.1, 27.6], - 433: [53.2, 27.6], - 434: [53.4, 27.6], - 435: [53.6, 27.6], - 436: [53.7, 27.6], - 437: [53.9, 27.6], - 438: [54.1, 27.7], - 439: [54.2, 27.7], - 440: [54.3, 27.7], - 441: [54.5, 27.7], - 442: [54.7, 27.7], - 443: [54.8, 27.7], - 444: [55, 27.7], - 445: [55.2, 27.7], - 446: [55.3, 27.7], - 447: [55.5, 27.7], - 448: [55.7, 27.7], - 449: [55.8, 27.8], - 450: [56, 27.8], - 451: [56.2, 27.8], - 452: [56.3, 27.8], - 453: [56.5, 27.8], - 454: [56.7, 27.8], - 455: [56.8, 27.8], - 456: [57, 27.8], - 457: [57.2, 27.8], - 458: [57.3, 27.9], - 459: [57.4, 27.9], - 460: [57.6, 27.9], - 461: [57.8, 27.9], - 462: [57.9, 27.9], - 463: [58.1, 27.9], - 464: [58.3, 27.9], - 465: [58.4, 27.9], - 466: [58.6, 27.9], - 467: [58.8, 27.9], - 468: [59, 28], - 469: [59.1, 28], - 470: [59.2, 28], - 471: [59.4, 28], - 472: [59.6, 28], - 473: [59.7, 28], - 474: [60, 28], - 475: [60.1, 28], - 476: [60.2, 28], - 477: [60.4, 28], - 478: [60.6, 28.1], - 479: [60.7, 28.1], - 480: [60.9, 28.1], - 481: [60.1, 28.1], - 482: [60.3, 28.1], - 483: [61.4, 28.1], - 484: [61.5, 28.1], - 485: [61.7, 28.1], - 486: [61.9, 28.1], - 487: [62, 28.2], - 488: [62.2, 28.2], - 489: [62.3, 28.2], - 490: [62.5, 28.2], - 491: [62.7, 28.2], - 492: [62.8, 28.2], - 493: [63, 28.2], - 494: [63.2, 28.2], - 495: [63.3, 28.2], - 496: [63.4, 28.2], - 497: [63.6, 28.2], - 498: [63.8, 28.3], - 499: [63.9, 28.3], - 500: [64.1, 28.3], - }; - const input = Math.min(Math.max(Math.round(m), 140), 500); - const toReturn = table[input]; - return [Math.round(toReturn[1]), Math.round(toReturn[0])]; -} diff --git a/src/utils.ts b/src/utils.ts index 1543c916..34df7a3a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,648 @@ -/* Copyright(C) 2017-2023, donavanbecker (https://github.com/donavanbecker). All rights reserved. +/* Copyright(C) 2017-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. * * util.ts: @switchbot/homebridge-switchbot platform class. */ +export enum SwitchBotModel { + HubMini = 'W0202200', + HubPlus = 'SwitchBot Hub S1', + Hub2 = 'W3202100', + Bot = 'SwitchBot S1', + Curtain = 'W0701600', + Curtain3 = 'W2400000', + Humidifier = 'W0801800', + Plug = 'SP11', // Currently only available in Japan + Meter = 'SwitchBot MeterTH S1', + MeterPlusJP = 'W2201500', + MeterPlusUS = 'W2301500', + OutdoorMeter = 'W3400010', + MotionSensor = 'W1101500', + ContactSensor = 'W1201500', + ColorBulb = 'W1401400', + StripLight = 'W1701100', + PlugMiniUS = 'W1901400/W1901401', + PlugMiniJP = 'W2001400/W2001401', + Lock = 'W1601700', + LockPro = 'W3500000', + Keypad = 'W2500010', + KeypadTouch = 'W2500020', + K10 = 'K10+', + WoSweeper = 'WoSweeper', + WoSweeperMini = 'WoSweeperMini', + RobotVacuumCleanerS1 = 'W3011000', // Currently only available in Japan. + RobotVacuumCleanerS1Plus = 'W3011010', // Currently only available in Japan. + RobotVacuumCleanerS10 = 'W3211800', + Remote = 'Remote', + UniversalRemote = 'UniversalRemote', + CeilingLight = 'W2612230/W2612240', // Currently only available in Japan. + CeilingLightPro = 'W2612210/W2612220', // Currently only available in Japan. + IndoorCam = 'W1301200', + PanTiltCam = 'W1801200', + PanTiltCam2K = 'W3101100', + BlindTilt = 'W2701600', + BatteryCirculatorFan = 'W3800510', + WaterDetector = 'W4402000', + Unknown = 'Unknown', +} + +export enum SwitchBotBLEModel { + Bot = 'H', + Curtain = 'c', + Curtain3 = '{', + Humidifier = 'e', + Meter = 'T', + MeterPlus = 'i', + OutdoorMeter = 'w', + MotionSensor = 's', + ContactSensor = 'd', + ColorBulb = 'u', + StripLight = 'r', + PlugMiniUS = 'g', + PlugMiniJP = 'j', + Lock = 'o', + Remote = '', + CeilingLight = 'q', // Currently only available in Japan. + CeilingLightPro = 'n', // Currently only available in Japan. + BlindTilt = 'x', + Unknown = 'Unknown', +} + +export enum BlindTiltMappingMode { + OnlyUp = 'only_up', + OnlyDown = 'only_down', + DownAndUp = 'down_and_up', + UpAndDown = 'up_and_down', + UseTiltForDirection = 'use_tilt_for_direction', +} + export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } + +/** + * Converts the value to celsius if the temperature units are in Fahrenheit +**/ +export function convertUnits(value: number, unit: string, convert?: string): number { + if (unit === 'CELSIUS' && convert === 'CELSIUS') { + return Math.round((value * 9) / 5 + 32); + } else if (unit === 'FAHRENHEIT' && convert === 'FAHRENHEIT') { + // celsius should be to the nearest 0.5 degree + return Math.round((5 / 9) * (value - 32) * 2) / 2; + } + return value; +} + + +export function rgb2hs(r: any, g: any, b: any) { + /** + * Credit: + * https://github.com/WickyNilliams/pure-color + **/ + r = parseInt(r); + g = parseInt(g); + b = parseInt(b); + const min = Math.min(r, g, b); + const max = Math.max(r, g, b); + const delta = max - min; + let h; + let s; + if (max === 0) { + s = 0; + } else { + s = (delta / max) * 100; + } + if (max === min) { + h = 0; + } else if (r === max) { + h = (g - b) / delta; + } else if (g === max) { + h = 2 + (b - r) / delta; + } else if (b === max) { + h = 4 + (r - g) / delta; + } + h = Math.min(h * 60, 360); + + if (h < 0) { + h += 360; + } + return [Math.round(h), Math.round(s)]; +} + +export function hs2rgb(h: any, s: any) { + /* + Credit: + https://github.com/WickyNilliams/pure-color + */ + h = parseInt(h) / 60; + s = parseInt(s) / 100; + const f = h - Math.floor(h); + const p = 255 * (1 - s); + const q = 255 * (1 - s * f); + const t = 255 * (1 - s * (1 - f)); + let rgb; + switch (Math.floor(h) % 6) { + case 0: + rgb = [255, t, p]; + break; + case 1: + rgb = [q, 255, p]; + break; + case 2: + rgb = [p, 255, t]; + break; + case 3: + rgb = [p, q, 255]; + break; + case 4: + rgb = [t, p, 255]; + break; + case 5: + rgb = [255, p, q]; + break; + } + if (rgb[0] === 255) { + rgb[1] *= 0.8; + rgb[2] *= 0.8; + if (rgb[1] <= 25 && rgb[2] <= 25) { + rgb[1] = 0; + rgb[2] = 0; + } + } + return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2])]; +} + +export function k2rgb(k: number) { + // Set kelvin to nearest 100, between 2000 and 7100 + k = Math.round(k / 100) * 100; + k = Math.max(Math.min(k, 7100), 2000); + + // k should now appear in our table of kelvin to rgb + const values = { + 2000: [255, 141, 11], + 2100: [255, 146, 29], + 2200: [255, 147, 44], + 2300: [255, 152, 54], + 2400: [255, 157, 63], + 2500: [255, 166, 69], + 2600: [255, 170, 77], + 2700: [255, 174, 84], + 2800: [255, 173, 94], + 2900: [255, 177, 101], + 3000: [255, 180, 107], + 3100: [255, 189, 111], + 3200: [255, 187, 120], + 3300: [255, 195, 124], + 3400: [255, 198, 130], + 3500: [255, 201, 135], + 3600: [255, 203, 141], + 3700: [255, 206, 146], + 3800: [255, 204, 153], + 3900: [255, 206, 159], + 4000: [255, 213, 161], + 4100: [255, 215, 166], + 4200: [255, 217, 171], + 4300: [255, 219, 175], + 4400: [255, 221, 180], + 4500: [255, 223, 184], + 4600: [255, 225, 188], + 4700: [255, 226, 192], + 4800: [255, 228, 196], + 4900: [255, 229, 200], + 5000: [255, 231, 204], + 5100: [255, 230, 210], + 5200: [255, 234, 211], + 5300: [255, 235, 215], + 5400: [255, 237, 218], + 5500: [255, 236, 224], + 5700: [255, 240, 228], + 5800: [255, 241, 231], + 5900: [255, 243, 234], + 6000: [255, 244, 237], + 6100: [255, 245, 240], + 6200: [255, 246, 243], + 6300: [255, 247, 247], + 6400: [255, 248, 251], + 6500: [255, 249, 253], + 6600: [254, 249, 255], + 6700: [252, 247, 255], + 6800: [249, 246, 255], + 6900: [247, 245, 255], + 7000: [245, 243, 255], + 7100: [243, 242, 255], + }; + + // Return the value + return values[k]; +} + +export function m2hs(m) { + /* + Credit: + https://github.com/homebridge/HAP-NodeJS + */ + const table = { + 100: [19, 222.1], + 101: [18.7, 222.2], + 102: [18.4, 222.3], + 103: [18.2, 222.3], + 104: [17.9, 222.4], + 105: [17.6, 222.5], + 106: [17.3, 222.7], + 107: [17, 222.8], + 108: [16.7, 222.9], + 109: [16.4, 223], + 110: [16.1, 223.2], + 111: [15.8, 223.3], + 112: [15.4, 223.4], + 113: [15.2, 223.6], + 114: [14.9, 223.8], + 115: [14.7, 223.9], + 116: [14.3, 224.1], + 117: [14.1, 224.2], + 118: [13.8, 224.4], + 119: [13.5, 224.6], + 120: [13.2, 224.8], + 121: [12.9, 225], + 122: [12.5, 225.3], + 123: [12.2, 225.6], + 124: [11.8, 225.9], + 125: [11.4, 226.3], + 126: [11.1, 226.7], + 127: [10.7, 227.1], + 128: [10.3, 227.6], + 129: [9.9, 228], + 130: [9.6, 228.5], + 131: [9.3, 229.1], + 132: [8.9, 229.6], + 133: [8.5, 230.2], + 134: [8.2, 230.9], + 135: [7.8, 231.6], + 136: [7.5, 232.5], + 137: [7.1, 233.5], + 138: [6.7, 234.6], + 139: [6.3, 235.8], + 140: [6, 237.1], + 141: [5.6, 238.9], + 142: [5.2, 240.9], + 143: [5, 242.9], + 144: [4.8, 244.9], + 145: [4.6, 246.9], + 146: [4.4, 249.3], + 147: [4.3, 251.9], + 148: [4.1, 254.9], + 149: [3.9, 258], + 150: [3.7, 261.8], + 151: [3.4, 265.9], + 152: [3.2, 271], + 153: [3, 276.4], + 154: [2.8, 283.6], + 155: [2.6, 290.4], + 156: [2.3, 295.3], + 157: [2.1, 300], + 158: [1.9, 300], + 159: [1.6, 300], + 160: [1.4, 195.8], + 161: [1.2, 84.3], + 162: [1.3, 58.2], + 163: [1.5, 55.9], + 164: [1.7, 53.2], + 165: [1.9, 50.2], + 166: [2.1, 47.1], + 167: [2.4, 44.5], + 168: [2.6, 42.6], + 169: [2.9, 40.9], + 170: [3.1, 39.5], + 171: [3.4, 38.3], + 172: [3.7, 37.3], + 173: [3.9, 36.5], + 174: [4.2, 35.7], + 175: [4.4, 35.1], + 176: [4.6, 34.5], + 177: [4.9, 34], + 178: [5.1, 33.5], + 179: [5.3, 33], + 180: [5.6, 32.7], + 181: [5.8, 32.3], + 182: [6, 32], + 183: [6.3, 31.7], + 184: [6.5, 31.4], + 185: [6.7, 31.2], + 186: [7, 30.9], + 187: [7.2, 30.7], + 188: [7.4, 30.5], + 189: [7.6, 30.3], + 190: [7.9, 30.1], + 191: [8.1, 29.9], + 192: [8.4, 29.7], + 193: [8.6, 29.6], + 194: [8.9, 29.5], + 195: [9.1, 29.3], + 196: [9.4, 29.2], + 197: [9.6, 29.1], + 198: [9.8, 29], + 199: [10, 28.9], + 200: [10.2, 28.7], + 201: [10.5, 28.7], + 202: [10.7, 28.6], + 203: [11, 28.5], + 204: [11.2, 28.4], + 205: [11.4, 28.3], + 206: [11.6, 28.3], + 207: [11.8, 28.2], + 208: [12.1, 28.1], + 209: [12.3, 28.1], + 210: [12.5, 28], + 211: [12.7, 28], + 212: [12.9, 27.9], + 213: [13.2, 27.8], + 214: [13.4, 27.8], + 215: [13.6, 27.7], + 216: [13.8, 27.7], + 217: [14, 27.7], + 218: [14.3, 27.6], + 219: [14.5, 27.6], + 220: [14.7, 27.5], + 221: [14.9, 27.5], + 222: [15.1, 27.5], + 223: [15.3, 27.4], + 224: [15.5, 27.4], + 225: [15.8, 27.4], + 226: [16, 27.3], + 227: [16.2, 27.3], + 228: [16.4, 27.3], + 229: [16.6, 27.3], + 230: [16.8, 27.2], + 231: [17, 27.2], + 232: [17.2, 27.2], + 233: [17.4, 27.2], + 234: [17.6, 27.2], + 235: [17.8, 27.1], + 236: [18, 27.1], + 237: [18.2, 27.1], + 238: [18.4, 27.1], + 239: [18.7, 27.1], + 240: [18.8, 27], + 241: [19, 27], + 242: [19.2, 27], + 243: [19.4, 27], + 244: [19.6, 27], + 245: [19.8, 27], + 246: [20, 27], + 247: [20.3, 26.9], + 248: [20.5, 26.9], + 249: [20.6, 26.9], + 250: [20.8, 26.9], + 251: [21, 26.9], + 252: [21.3, 26.9], + 253: [21.5, 26.9], + 254: [21.6, 26.9], + 255: [21.8, 26.8], + 256: [22, 26.8], + 257: [22.2, 26.8], + 258: [22.4, 26.8], + 259: [22.6, 26.8], + 260: [22.8, 26.8], + 261: [23, 26.8], + 262: [23.2, 26.8], + 263: [23.4, 26.8], + 264: [23.6, 26.8], + 265: [23.8, 26.8], + 266: [24, 26.8], + 267: [24.1, 26.8], + 268: [24.3, 26.8], + 269: [24.5, 26.8], + 270: [24.7, 26.8], + 271: [24.8, 26.8], + 272: [25.1, 26.7], + 273: [25.3, 26.7], + 274: [25.4, 26.7], + 275: [25.6, 26.7], + 276: [25.8, 26.7], + 277: [26, 26.7], + 278: [26.1, 26.7], + 279: [26.3, 26.7], + 280: [26.5, 26.7], + 281: [26.7, 26.7], + 282: [26.9, 26.7], + 283: [27.1, 26.7], + 284: [27.3, 26.7], + 285: [27.5, 26.7], + 286: [27.7, 26.7], + 287: [27.8, 26.7], + 288: [28, 26.7], + 289: [28.2, 26.7], + 290: [28.4, 26.7], + 291: [28.6, 26.7], + 292: [28.8, 26.7], + 293: [28.9, 26.7], + 294: [29.1, 26.7], + 295: [29.3, 26.7], + 296: [29.5, 26.7], + 297: [29.6, 26.7], + 298: [29.8, 26.7], + 299: [30, 26.7], + 300: [30.2, 26.7], + 301: [30.4, 26.7], + 302: [30.5, 26.7], + 303: [30.7, 26.7], + 304: [30.9, 26.7], + 305: [31.1, 26.7], + 306: [31.2, 26.7], + 307: [31.4, 26.7], + 308: [31.6, 26.7], + 309: [31.8, 26.8], + 310: [31.9, 26.8], + 311: [32.1, 26.8], + 312: [32.3, 26.8], + 313: [32.5, 26.8], + 314: [32.6, 26.8], + 315: [32.8, 26.8], + 316: [33, 26.8], + 317: [33.2, 26.8], + 318: [33.3, 26.8], + 319: [33.5, 26.8], + 320: [33.7, 26.8], + 321: [33.8, 26.8], + 322: [34, 26.8], + 323: [34.2, 26.8], + 324: [34.4, 26.8], + 325: [34.5, 26.8], + 326: [34.7, 26.8], + 327: [34.9, 26.8], + 328: [35.1, 26.8], + 329: [35.2, 26.8], + 330: [35.4, 26.8], + 331: [35.5, 26.8], + 332: [35.7, 26.8], + 333: [35.9, 26.8], + 334: [36.1, 26.8], + 335: [36.3, 26.9], + 336: [36.5, 26.9], + 337: [36.7, 26.9], + 338: [36.9, 26.9], + 339: [37.1, 26.9], + 340: [37.2, 26.9], + 341: [37.4, 26.9], + 342: [37.5, 26.9], + 343: [37.7, 26.9], + 344: [37.9, 26.9], + 345: [38.1, 26.9], + 346: [38.3, 26.9], + 347: [38.5, 26.9], + 348: [38.7, 26.9], + 349: [38.9, 26.9], + 350: [39, 26.9], + 351: [39.2, 26.9], + 352: [39.3, 27], + 353: [39.5, 27], + 354: [39.7, 27], + 355: [39.9, 27], + 356: [40.1, 27], + 357: [40.2, 27], + 358: [40.4, 27], + 359: [40.6, 27], + 360: [40.8, 27], + 361: [40.9, 27], + 362: [41.1, 27], + 363: [41.2, 27], + 364: [41.4, 27], + 365: [41.6, 27], + 366: [41.8, 27], + 367: [42, 27], + 368: [42.1, 27.1], + 369: [42.3, 27.1], + 370: [42.4, 27.1], + 371: [42.6, 27.1], + 372: [42.8, 27.1], + 373: [43, 27.1], + 374: [43.1, 27.1], + 375: [43.2, 27.1], + 376: [43.4, 27.1], + 377: [43.6, 27.1], + 378: [43.8, 27.1], + 379: [43.9, 27.1], + 380: [44.1, 27.1], + 381: [44.3, 27.2], + 382: [44.4, 27.2], + 383: [44.6, 27.2], + 384: [44.7, 27.2], + 385: [44.9, 27.2], + 386: [45.1, 27.2], + 387: [45.3, 27.2], + 388: [45.5, 27.2], + 389: [45.6, 27.2], + 390: [45.8, 27.2], + 391: [46, 27.2], + 392: [46.2, 27.2], + 393: [46.4, 27.3], + 394: [46.5, 27.3], + 395: [46.7, 27.3], + 396: [46.9, 27.3], + 397: [47.1, 27.3], + 398: [47.2, 27.3], + 399: [47.4, 27.3], + 400: [47.6, 27.3], + 401: [47.7, 27.3], + 402: [47.9, 27.3], + 403: [48.1, 27.3], + 404: [48.3, 27.3], + 405: [48.5, 27.4], + 406: [48.7, 27.4], + 407: [48.8, 27.4], + 408: [49, 27.4], + 409: [49.2, 27.4], + 410: [49.4, 27.4], + 411: [49.6, 27.4], + 412: [49.7, 27.4], + 413: [49.9, 27.4], + 414: [50.1, 27.4], + 415: [50.2, 27.4], + 416: [50.4, 27.4], + 417: [50.6, 27.5], + 418: [50.7, 27.5], + 419: [50.9, 27.5], + 420: [51.1, 27.5], + 421: [51.2, 27.5], + 422: [51.4, 27.5], + 423: [51.6, 27.5], + 424: [51.7, 27.5], + 425: [51.9, 27.5], + 426: [52.1, 27.5], + 427: [51.2, 27.6], + 428: [52.4, 27.6], + 429: [52.5, 27.6], + 430: [52.7, 27.6], + 431: [52.9, 27.6], + 432: [53.1, 27.6], + 433: [53.2, 27.6], + 434: [53.4, 27.6], + 435: [53.6, 27.6], + 436: [53.7, 27.6], + 437: [53.9, 27.6], + 438: [54.1, 27.7], + 439: [54.2, 27.7], + 440: [54.3, 27.7], + 441: [54.5, 27.7], + 442: [54.7, 27.7], + 443: [54.8, 27.7], + 444: [55, 27.7], + 445: [55.2, 27.7], + 446: [55.3, 27.7], + 447: [55.5, 27.7], + 448: [55.7, 27.7], + 449: [55.8, 27.8], + 450: [56, 27.8], + 451: [56.2, 27.8], + 452: [56.3, 27.8], + 453: [56.5, 27.8], + 454: [56.7, 27.8], + 455: [56.8, 27.8], + 456: [57, 27.8], + 457: [57.2, 27.8], + 458: [57.3, 27.9], + 459: [57.4, 27.9], + 460: [57.6, 27.9], + 461: [57.8, 27.9], + 462: [57.9, 27.9], + 463: [58.1, 27.9], + 464: [58.3, 27.9], + 465: [58.4, 27.9], + 466: [58.6, 27.9], + 467: [58.8, 27.9], + 468: [59, 28], + 469: [59.1, 28], + 470: [59.2, 28], + 471: [59.4, 28], + 472: [59.6, 28], + 473: [59.7, 28], + 474: [60, 28], + 475: [60.1, 28], + 476: [60.2, 28], + 477: [60.4, 28], + 478: [60.6, 28.1], + 479: [60.7, 28.1], + 480: [60.9, 28.1], + 481: [60.1, 28.1], + 482: [60.3, 28.1], + 483: [61.4, 28.1], + 484: [61.5, 28.1], + 485: [61.7, 28.1], + 486: [61.9, 28.1], + 487: [62, 28.2], + 488: [62.2, 28.2], + 489: [62.3, 28.2], + 490: [62.5, 28.2], + 491: [62.7, 28.2], + 492: [62.8, 28.2], + 493: [63, 28.2], + 494: [63.2, 28.2], + 495: [63.3, 28.2], + 496: [63.4, 28.2], + 497: [63.6, 28.2], + 498: [63.8, 28.3], + 499: [63.9, 28.3], + 500: [64.1, 28.3], + }; + const input = Math.min(Math.max(Math.round(m), 140), 500); + const toReturn = table[input]; + return [Math.round(toReturn[1]), Math.round(toReturn[0])]; +}