@mp-consulting/homebridge-daikin-cloud 1.3.6 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +39 -1
- package/README.md +3 -1
- package/dist/src/accessories/air-conditioning-accessory.d.ts +2 -2
- package/dist/src/accessories/air-conditioning-accessory.d.ts.map +1 -1
- package/dist/src/accessories/air-conditioning-accessory.js.map +1 -1
- package/dist/src/accessories/altherma-accessory.d.ts +2 -2
- package/dist/src/accessories/altherma-accessory.d.ts.map +1 -1
- package/dist/src/accessories/altherma-accessory.js.map +1 -1
- package/dist/src/accessories/base-accessory.d.ts +5 -5
- package/dist/src/accessories/base-accessory.d.ts.map +1 -1
- package/dist/src/accessories/base-accessory.js +7 -4
- package/dist/src/accessories/base-accessory.js.map +1 -1
- package/dist/src/api/daikin-api.d.ts +25 -25
- package/dist/src/api/daikin-api.d.ts.map +1 -1
- package/dist/src/api/daikin-api.js +41 -31
- package/dist/src/api/daikin-api.js.map +1 -1
- package/dist/src/api/daikin-cloud.repository.d.ts.map +1 -1
- package/dist/src/api/daikin-cloud.repository.js.map +1 -1
- package/dist/src/api/daikin-controller.d.ts +41 -42
- package/dist/src/api/daikin-controller.d.ts.map +1 -1
- package/dist/src/api/daikin-controller.js +39 -39
- package/dist/src/api/daikin-controller.js.map +1 -1
- package/dist/src/api/daikin-device.d.ts +33 -31
- package/dist/src/api/daikin-device.d.ts.map +1 -1
- package/dist/src/api/daikin-device.js +40 -29
- package/dist/src/api/daikin-device.js.map +1 -1
- package/dist/src/api/daikin-mobile-oauth.d.ts +16 -16
- package/dist/src/api/daikin-mobile-oauth.d.ts.map +1 -1
- package/dist/src/api/daikin-mobile-oauth.js +32 -22
- package/dist/src/api/daikin-mobile-oauth.js.map +1 -1
- package/dist/src/api/daikin-oauth.d.ts +29 -29
- package/dist/src/api/daikin-oauth.d.ts.map +1 -1
- package/dist/src/api/daikin-oauth.js +45 -35
- package/dist/src/api/daikin-oauth.js.map +1 -1
- package/dist/src/api/daikin-schemas.d.ts +4 -4
- package/dist/src/api/daikin-schemas.js +3 -3
- package/dist/src/api/daikin-schemas.js.map +1 -1
- package/dist/src/api/daikin-types.js.map +1 -1
- package/dist/src/api/daikin-websocket.d.ts +31 -32
- package/dist/src/api/daikin-websocket.d.ts.map +1 -1
- package/dist/src/api/daikin-websocket.js +30 -30
- package/dist/src/api/daikin-websocket.js.map +1 -1
- package/dist/src/api/index.d.ts +1 -1
- package/dist/src/api/index.d.ts.map +1 -1
- package/dist/src/api/index.js +2 -1
- package/dist/src/api/index.js.map +1 -1
- package/dist/src/api/token-storage.d.ts +1 -1
- package/dist/src/api/token-storage.d.ts.map +1 -1
- package/dist/src/api/token-storage.js +20 -11
- package/dist/src/api/token-storage.js.map +1 -1
- package/dist/src/config/config-manager.d.ts +33 -33
- package/dist/src/config/config-manager.d.ts.map +1 -1
- package/dist/src/config/config-manager.js +33 -33
- package/dist/src/config/config-manager.js.map +1 -1
- package/dist/src/constants/api.constants.js.map +1 -1
- package/dist/src/device/accessory-factory.d.ts +10 -10
- package/dist/src/device/accessory-factory.d.ts.map +1 -1
- package/dist/src/device/accessory-factory.js +6 -6
- package/dist/src/device/accessory-factory.js.map +1 -1
- package/dist/src/device/capability-detector.d.ts +8 -8
- package/dist/src/device/capability-detector.d.ts.map +1 -1
- package/dist/src/device/capability-detector.js +6 -6
- package/dist/src/device/capability-detector.js.map +1 -1
- package/dist/src/device/capability-docs.d.ts +1 -1
- package/dist/src/device/capability-docs.d.ts.map +1 -1
- package/dist/src/device/capability-docs.js +1 -2
- package/dist/src/device/capability-docs.js.map +1 -1
- package/dist/src/device/profiles/device-profile.d.ts +1 -1
- package/dist/src/device/profiles/device-profile.d.ts.map +1 -1
- package/dist/src/device/profiles/device-profile.js +4 -4
- package/dist/src/device/profiles/device-profile.js.map +1 -1
- package/dist/src/features/base-feature.d.ts +2 -2
- package/dist/src/features/base-feature.d.ts.map +1 -1
- package/dist/src/features/base-feature.js +2 -3
- package/dist/src/features/base-feature.js.map +1 -1
- package/dist/src/features/feature-manager.d.ts +8 -8
- package/dist/src/features/feature-manager.d.ts.map +1 -1
- package/dist/src/features/feature-manager.js +5 -5
- package/dist/src/features/feature-manager.js.map +1 -1
- package/dist/src/features/modes/dry-operation-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/dry-operation-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/dry-operation-mode.feature.js.map +1 -1
- package/dist/src/features/modes/econo-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/econo-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/econo-mode.feature.js.map +1 -1
- package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/fan-only-operation-mode.feature.js.map +1 -1
- package/dist/src/features/modes/indoor-silent-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/indoor-silent-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/indoor-silent-mode.feature.js.map +1 -1
- package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/outdoor-silent-mode.feature.js.map +1 -1
- package/dist/src/features/modes/powerful-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/powerful-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/powerful-mode.feature.js.map +1 -1
- package/dist/src/features/modes/streamer-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/streamer-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/streamer-mode.feature.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/platform.d.ts +6 -5
- package/dist/src/platform.d.ts.map +1 -1
- package/dist/src/platform.js +2 -2
- package/dist/src/platform.js.map +1 -1
- package/dist/src/services/climate-control.service.d.ts +8 -2
- package/dist/src/services/climate-control.service.d.ts.map +1 -1
- package/dist/src/services/climate-control.service.js +53 -59
- package/dist/src/services/climate-control.service.js.map +1 -1
- package/dist/src/services/hot-water-tank.service.d.ts +6 -2
- package/dist/src/services/hot-water-tank.service.d.ts.map +1 -1
- package/dist/src/services/hot-water-tank.service.js +31 -34
- package/dist/src/services/hot-water-tank.service.js.map +1 -1
- package/dist/src/types/daikin-enums.js +12 -12
- package/dist/src/types/daikin-enums.js.map +1 -1
- package/dist/src/types/device-capabilities.d.ts +1 -1
- package/dist/src/types/device-capabilities.d.ts.map +1 -1
- package/dist/src/utils/log-context.d.ts +23 -23
- package/dist/src/utils/log-context.d.ts.map +1 -1
- package/dist/src/utils/log-context.js +28 -28
- package/dist/src/utils/log-context.js.map +1 -1
- package/dist/src/utils/strings.d.ts.map +1 -1
- package/dist/src/utils/strings.js.map +1 -1
- package/dist/src/utils/update-mapper.d.ts +16 -16
- package/dist/src/utils/update-mapper.d.ts.map +1 -1
- package/dist/src/utils/update-mapper.js +14 -14
- package/dist/src/utils/update-mapper.js.map +1 -1
- package/homebridge-ui/public/script.js +956 -897
- package/homebridge-ui/server.js +739 -695
- package/package.json +27 -24
- package/.claude/settings.json +0 -3
- package/.claude/settings.local.json +0 -24
- package/CHANGELOG.md +0 -114
- package/CLAUDE.md +0 -269
- package/config.md +0 -2
- package/docs/ARCHITECTURE.md +0 -645
- package/docs/IMPLEMENTATION_GUIDE.md +0 -899
- package/docs/IMPROVEMENTS_SUMMARY.md +0 -415
- package/docs/NEXT_STEPS.md +0 -368
- package/docs/Screenshot 2024-07-04 at 18.41.28.png +0 -0
- package/docs/TROUBLESHOOTING.md +0 -475
- package/docs/api-response-for-BRP069A8x.json +0 -520
- package/docs/api-response-for-BRP069C4x-2.json +0 -881
- package/docs/api-response-for-BRP069C4x.json +0 -916
- package/docs/api-response-for-altherma.json +0 -759
- package/docs/api-response-for-altherma2.json +0 -2735
- package/docs/api-response-with-multiple-devices-incl-heatpump.json +0 -2544
- package/docs/cr-insance-altherma-id-0.json +0 -834
- package/docs/mock-air-to-air-dx23.json +0 -759
- package/docs/mock-air-to-air-dx4.json +0 -1134
- package/docs/mock-airpurifier-with-humidifier.json +0 -732
- package/docs/mock-airpurifier.json +0 -450
- package/docs/mock-altherma-air-to-water-lan.json +0 -845
- package/docs/mock-altherma-air-to-water-wlan.json +0 -845
- package/docs/mock-d2cnd-gas-boiler.json +0 -649
- package/docs/setpointmode-vs-controlmode-vs-setpoints-vs-sensorydata.txt +0 -6
- package/images/fan-speed.jpeg +0 -0
- package/images/homekit-controls.jpeg +0 -0
- package/images/homekit-settings.jpeg +0 -0
- package/images/swing-mode.png +0 -0
- package/jest.config.ts +0 -21
- package/test/fixtures/altherma-crSense-2.ts +0 -834
- package/test/fixtures/altherma-fraction.ts +0 -718
- package/test/fixtures/altherma-heat-pump-2.ts +0 -479
- package/test/fixtures/altherma-heat-pump.ts +0 -757
- package/test/fixtures/altherma-miladcerkic-off.ts +0 -524
- package/test/fixtures/altherma-miladcerkic.ts +0 -524
- package/test/fixtures/altherma-v1ckoeln.ts +0 -644
- package/test/fixtures/altherma-with-embedded-id-zero.ts +0 -834
- package/test/fixtures/dx23-airco-2.ts +0 -343
- package/test/fixtures/dx23-airco.ts +0 -518
- package/test/fixtures/dx4-airco.ts +0 -914
- package/test/fixtures/unknown-jan.ts +0 -488
- package/test/fixtures/unknown-kitchen-guests.ts +0 -488
- package/test/hbConfig/.daikin-mobile-tokenset +0 -8
- package/test/hbConfig/.uix-dashboard.json +0 -1
- package/test/hbConfig/.uix-secrets +0 -1
- package/test/hbConfig/accessories/.cachedAccessories.bak +0 -1
- package/test/hbConfig/accessories/cachedAccessories +0 -1
- package/test/hbConfig/accessories/uiAccessoriesLayout.json +0 -1
- package/test/hbConfig/auth.json +0 -10
- package/test/hbConfig/backups/config-backups/config.json.1767953686461 +0 -25
- package/test/hbConfig/backups/config-backups/config.json.1767953695236 +0 -29
- package/test/hbConfig/backups/config-backups/config.json.1767953814763 +0 -29
- package/test/hbConfig/backups/config-backups/config.json.1767953823101 +0 -29
- package/test/hbConfig/backups/config-backups/config.json.1767954822835 +0 -29
- package/test/hbConfig/backups/config-backups/config.json.1767954859218 +0 -29
- package/test/hbConfig/backups/config-backups/config.json.1767960145503 +0 -33
- package/test/hbConfig/backups/config-backups/config.json.1767960168068 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960170333 +0 -46
- package/test/hbConfig/backups/config-backups/config.json.1767960172731 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960179323 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960182114 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960189302 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960195194 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960197301 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960199151 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960199667 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960329839 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960334503 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960336208 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767960338537 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767963223953 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767963241753 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767963252785 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767963463944 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767963834475 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767963838474 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767963843066 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767965217715 +0 -44
- package/test/hbConfig/backups/config-backups/config.json.1767965419624 +0 -25
- package/test/hbConfig/backups/config-backups/config.json.1767965870934 +0 -32
- package/test/hbConfig/backups/config-backups/config.json.1767977675045 +0 -32
- package/test/hbConfig/backups/config-backups/config.json.1767977677222 +0 -33
- package/test/hbConfig/backups/config-backups/config.json.1767977710226 +0 -33
- package/test/hbConfig/backups/config-backups/config.json.1767977741397 +0 -33
- package/test/hbConfig/backups/config-backups/config.json.1767977977093 +0 -35
- package/test/hbConfig/backups/config-backups/config.json.1767977981773 +0 -35
- package/test/hbConfig/backups/config-backups/config.json.1767977986514 +0 -35
- package/test/hbConfig/backups/config-backups/config.json.1767977991174 +0 -35
- package/test/hbConfig/backups/config-backups/config.json.1767979424487 +0 -35
- package/test/hbConfig/backups/config-backups/config.json.1767979424987 +0 -35
- package/test/hbConfig/backups/config-backups/config.json.1767979432646 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767979433150 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767979436933 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767979437438 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767979441676 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767979442180 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767979466735 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767979903636 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767979904135 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767979906606 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767979907108 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767988702341 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767988702837 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767988713159 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767988713664 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767988918139 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767988918639 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767988921120 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767988921624 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767988930307 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767988935070 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767988935574 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767989710262 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767989710760 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767989729668 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767990295225 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767990479921 +0 -47
- package/test/hbConfig/backups/config-backups/config.json.1767990481702 +0 -49
- package/test/hbConfig/backups/instance-backups/homebridge-backup-1E4A432551BA.1768010187391.tar.gz +0 -0
- package/test/hbConfig/backups/instance-backups/homebridge-backup-1E4A432551BA.1768096587387.tar.gz +0 -0
- package/test/hbConfig/backups/instance-backups/homebridge-backup-1E4A432551BA.1768182987404.tar.gz +0 -0
- package/test/hbConfig/config.json +0 -47
- package/test/hbConfig/daikin-cloud-certs/server.crt +0 -22
- package/test/hbConfig/daikin-cloud-certs/server.key +0 -28
- package/test/hbConfig/persist/AccessoryInfo.1E4A432551BA.json +0 -1
- package/test/hbConfig/persist/IdentifierCache.1E4A432551BA.json +0 -1
- package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14758 +0 -1
- package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14759 +0 -1
- package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14760 +0 -1
- package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14761 +0 -1
- package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14762 +0 -1
- package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14764 +0 -1
- package/test/helpers/test-isolation.ts +0 -228
- package/test/integration/air-conditioning.test.ts +0 -396
- package/test/integration/altherma.test.ts +0 -279
- package/test/integration/platform.test.ts +0 -118
- package/test/mobile-tokens.json +0 -8
- package/test/mocks/index.ts +0 -27
- package/test/test-gigya-auth.js +0 -443
- package/test/test-mobile-oauth.js +0 -175
- package/test/test-websocket-mobile.js +0 -123
- package/test/test-websocket.js +0 -116
- package/test/unit/api/__snapshots__/daikinCloud.test.ts.snap +0 -1320
- package/test/unit/api/daikin-api.test.ts +0 -442
- package/test/unit/api/daikin-cloud-repository.test.ts +0 -107
- package/test/unit/api/daikin-oauth.test.ts +0 -214
- package/test/unit/api/daikinCloud.test.ts +0 -12
- package/test/unit/api/token-storage.test.ts +0 -90
- package/test/unit/config/config-manager.test.ts +0 -271
- package/test/unit/device/daikin-device.test.ts +0 -73
- package/test/unit/services/hot-water-tank.service.test.ts +0 -123
- package/test/unit/utils/log-context.test.ts +0 -271
- package/test/unit/utils/update-mapper.test.ts +0 -404
package/homebridge-ui/server.js
CHANGED
|
@@ -24,69 +24,69 @@ const CLIMATE_CONTROL_IDS = ['climateControl', 'climateControlMainZone', 'climat
|
|
|
24
24
|
// =============================================================================
|
|
25
25
|
|
|
26
26
|
const SSLUtils = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
isIPAddress(str) {
|
|
28
|
+
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
29
|
+
const ipv6Pattern = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
|
30
|
+
return ipv4Pattern.test(str) || ipv6Pattern.test(str);
|
|
31
|
+
},
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
34
|
* Validate that a hostname is safe for use in shell commands.
|
|
35
35
|
* Only allows alphanumeric, dots, hyphens, and colons (for IPv6).
|
|
36
36
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
validateHostname(hostname) {
|
|
38
|
+
if (!hostname || typeof hostname !== 'string') {
|
|
39
|
+
throw new Error('Hostname is required');
|
|
40
|
+
}
|
|
41
|
+
if (!/^[a-zA-Z0-9.:_-]+$/.test(hostname)) {
|
|
42
|
+
throw new Error('Invalid hostname: contains disallowed characters');
|
|
43
|
+
}
|
|
44
|
+
if (hostname.length > 253) {
|
|
45
|
+
throw new Error('Hostname too long (max 253 characters)');
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
generateCert(hostname, certDir) {
|
|
50
|
+
this.validateHostname(hostname);
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const keyPath = resolve(certDir, 'server.key');
|
|
53
|
+
const certPath = resolve(certDir, 'server.crt');
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
if (!fs.existsSync(certDir)) {
|
|
56
|
+
fs.mkdirSync(certDir, { recursive: true });
|
|
57
|
+
}
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
60
|
+
try {
|
|
61
|
+
return {
|
|
62
|
+
key: fs.readFileSync(keyPath, 'utf8'),
|
|
63
|
+
cert: fs.readFileSync(certPath, 'utf8'),
|
|
64
|
+
};
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// Regenerate if can't read
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
try {
|
|
71
|
+
execSync(`openssl genrsa -out "${keyPath}" 2048`, { stdio: 'pipe' });
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
const isIP = this.isIPAddress(hostname);
|
|
74
|
+
const sanValue = isIP ? `IP:${hostname}` : `DNS:${hostname}`;
|
|
75
|
+
const subj = `/CN=${hostname}/O=Homebridge Daikin Cloud/C=US`;
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
execSync(
|
|
78
|
+
`openssl req -new -x509 -key "${keyPath}" -out "${certPath}" -days 365 -subj "${subj}" -addext "subjectAltName=${sanValue}"`,
|
|
79
|
+
{ stdio: 'pipe' },
|
|
80
|
+
);
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
return {
|
|
83
|
+
key: fs.readFileSync(keyPath, 'utf8'),
|
|
84
|
+
cert: fs.readFileSync(certPath, 'utf8'),
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`Failed to generate SSL certificate: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
90
|
};
|
|
91
91
|
|
|
92
92
|
// =============================================================================
|
|
@@ -94,48 +94,50 @@ const SSLUtils = {
|
|
|
94
94
|
// =============================================================================
|
|
95
95
|
|
|
96
96
|
const TokenManager = {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (!tokenSet || !tokenSet.access_token) {
|
|
122
|
-
return { authenticated: false, message: 'Not authenticated' };
|
|
123
|
-
}
|
|
97
|
+
load(filePath) {
|
|
98
|
+
try {
|
|
99
|
+
if (fs.existsSync(filePath)) {
|
|
100
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Error loading token set:', error.message);
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
save(filePath, tokenSet) {
|
|
109
|
+
fs.writeFileSync(filePath, JSON.stringify(tokenSet, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
delete(filePath) {
|
|
113
|
+
try {
|
|
114
|
+
fs.unlinkSync(filePath);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.code !== 'ENOENT') {
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
124
121
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
getStatus(tokenSet) {
|
|
123
|
+
if (!tokenSet || !tokenSet.access_token) {
|
|
124
|
+
return { authenticated: false, message: 'Not authenticated' };
|
|
125
|
+
}
|
|
128
126
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
127
|
+
const expiresAt = tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000) : null;
|
|
128
|
+
const isExpired = expiresAt ? expiresAt < new Date() : false;
|
|
129
|
+
const hasRefreshToken = !!tokenSet.refresh_token;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
authenticated: true,
|
|
133
|
+
isExpired,
|
|
134
|
+
canRefresh: hasRefreshToken,
|
|
135
|
+
expiresAt: expiresAt ? expiresAt.toISOString() : null,
|
|
136
|
+
message: isExpired
|
|
137
|
+
? (hasRefreshToken ? 'Token expired, will refresh automatically' : 'Token expired, re-authentication required')
|
|
138
|
+
: 'Authenticated',
|
|
139
|
+
};
|
|
140
|
+
},
|
|
139
141
|
};
|
|
140
142
|
|
|
141
143
|
// =============================================================================
|
|
@@ -143,98 +145,122 @@ const TokenManager = {
|
|
|
143
145
|
// =============================================================================
|
|
144
146
|
|
|
145
147
|
const DeviceExtractor = {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
148
|
+
getManagementPoint(device, embeddedId) {
|
|
149
|
+
return device.managementPoints?.find(mp => mp.embeddedId === embeddedId) || null;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
getClimateControlPoint(device) {
|
|
153
|
+
for (const id of CLIMATE_CONTROL_IDS) {
|
|
154
|
+
const mp = this.getManagementPoint(device, id);
|
|
155
|
+
if (mp) {
|
|
156
|
+
return mp;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
extractName(device) {
|
|
163
|
+
const climateControl = this.getClimateControlPoint(device);
|
|
164
|
+
return climateControl?.name?.value || device.id || 'Unknown Device';
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
extractModel(device) {
|
|
168
|
+
const gateway = this.getManagementPoint(device, 'gateway');
|
|
169
|
+
return gateway?.modelInfo?.value || device.deviceModel || 'Unknown Model';
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
extractType(device) {
|
|
173
|
+
if (!device.managementPoints) {
|
|
174
|
+
return device.type || 'Unknown Type';
|
|
175
|
+
}
|
|
167
176
|
|
|
168
|
-
|
|
169
|
-
|
|
177
|
+
for (const mp of device.managementPoints) {
|
|
178
|
+
if (mp.embeddedId === 'climateControl') {
|
|
179
|
+
return 'Climate Control';
|
|
180
|
+
}
|
|
181
|
+
if (mp.embeddedId === 'domesticHotWaterTank') {
|
|
182
|
+
return 'Hot Water Tank';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return device.type || 'Unknown Type';
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
isOnline(device) {
|
|
189
|
+
return device.isCloudConnectionUp?.value ?? false;
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
extractRoomTemp(device) {
|
|
193
|
+
const climateControl = this.getClimateControlPoint(device);
|
|
194
|
+
const roomTemp = climateControl?.sensoryData?.value?.roomTemperature;
|
|
195
|
+
return roomTemp?.value !== undefined ? `${roomTemp.value}${roomTemp.unit || '°C'}` : null;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
extractOutdoorTemp(device) {
|
|
199
|
+
const climateControl = this.getClimateControlPoint(device);
|
|
200
|
+
const outdoorTemp = climateControl?.sensoryData?.value?.outdoorTemperature;
|
|
201
|
+
return outdoorTemp?.value !== undefined ? `${outdoorTemp.value}${outdoorTemp.unit || '°C'}` : null;
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
extractOperationMode(device) {
|
|
205
|
+
const climateControl = this.getClimateControlPoint(device);
|
|
206
|
+
return climateControl?.operationMode?.value || null;
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
extractPowerState(device) {
|
|
210
|
+
const climateControl = this.getClimateControlPoint(device);
|
|
211
|
+
return climateControl?.onOffMode?.value || null;
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
extractFeatures(device) {
|
|
215
|
+
const features = [];
|
|
216
|
+
if (!device.managementPoints) {
|
|
217
|
+
return features;
|
|
218
|
+
}
|
|
170
219
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
220
|
+
for (const mp of device.managementPoints) {
|
|
221
|
+
if (CLIMATE_CONTROL_IDS.includes(mp.embeddedId)) {
|
|
222
|
+
if (mp.onOffMode) {
|
|
223
|
+
features.push('Power');
|
|
174
224
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
isOnline(device) {
|
|
179
|
-
return device.isCloudConnectionUp?.value ?? false;
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
extractRoomTemp(device) {
|
|
183
|
-
const climateControl = this.getClimateControlPoint(device);
|
|
184
|
-
const roomTemp = climateControl?.sensoryData?.value?.roomTemperature;
|
|
185
|
-
return roomTemp?.value !== undefined ? `${roomTemp.value}${roomTemp.unit || '°C'}` : null;
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
extractOutdoorTemp(device) {
|
|
189
|
-
const climateControl = this.getClimateControlPoint(device);
|
|
190
|
-
const outdoorTemp = climateControl?.sensoryData?.value?.outdoorTemperature;
|
|
191
|
-
return outdoorTemp?.value !== undefined ? `${outdoorTemp.value}${outdoorTemp.unit || '°C'}` : null;
|
|
192
|
-
},
|
|
193
|
-
|
|
194
|
-
extractOperationMode(device) {
|
|
195
|
-
const climateControl = this.getClimateControlPoint(device);
|
|
196
|
-
return climateControl?.operationMode?.value || null;
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
extractPowerState(device) {
|
|
200
|
-
const climateControl = this.getClimateControlPoint(device);
|
|
201
|
-
return climateControl?.onOffMode?.value || null;
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
extractFeatures(device) {
|
|
205
|
-
const features = [];
|
|
206
|
-
if (!device.managementPoints) return features;
|
|
207
|
-
|
|
208
|
-
for (const mp of device.managementPoints) {
|
|
209
|
-
if (CLIMATE_CONTROL_IDS.includes(mp.embeddedId)) {
|
|
210
|
-
if (mp.onOffMode) features.push('Power');
|
|
211
|
-
if (mp.temperatureControl) features.push('Temperature');
|
|
212
|
-
if (mp.operationMode) features.push('Mode');
|
|
213
|
-
if (mp.fanControl) features.push('Fan');
|
|
214
|
-
if (mp.sensoryData) features.push('Sensors');
|
|
215
|
-
}
|
|
216
|
-
if (mp.embeddedId === 'domesticHotWaterTank') {
|
|
217
|
-
if (mp.onOffMode) features.push('Hot Water');
|
|
218
|
-
if (mp.temperatureControl) features.push('Water Temp');
|
|
219
|
-
}
|
|
225
|
+
if (mp.temperatureControl) {
|
|
226
|
+
features.push('Temperature');
|
|
220
227
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
228
|
+
if (mp.operationMode) {
|
|
229
|
+
features.push('Mode');
|
|
230
|
+
}
|
|
231
|
+
if (mp.fanControl) {
|
|
232
|
+
features.push('Fan');
|
|
233
|
+
}
|
|
234
|
+
if (mp.sensoryData) {
|
|
235
|
+
features.push('Sensors');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (mp.embeddedId === 'domesticHotWaterTank') {
|
|
239
|
+
if (mp.onOffMode) {
|
|
240
|
+
features.push('Hot Water');
|
|
241
|
+
}
|
|
242
|
+
if (mp.temperatureControl) {
|
|
243
|
+
features.push('Water Temp');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return features;
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
extractAll(device) {
|
|
251
|
+
return {
|
|
252
|
+
id: device.id,
|
|
253
|
+
name: this.extractName(device),
|
|
254
|
+
model: this.extractModel(device),
|
|
255
|
+
type: this.extractType(device),
|
|
256
|
+
online: this.isOnline(device),
|
|
257
|
+
features: this.extractFeatures(device),
|
|
258
|
+
roomTemp: this.extractRoomTemp(device),
|
|
259
|
+
outdoorTemp: this.extractOutdoorTemp(device),
|
|
260
|
+
operationMode: this.extractOperationMode(device),
|
|
261
|
+
powerState: this.extractPowerState(device),
|
|
262
|
+
};
|
|
263
|
+
},
|
|
238
264
|
};
|
|
239
265
|
|
|
240
266
|
// =============================================================================
|
|
@@ -242,83 +268,83 @@ const DeviceExtractor = {
|
|
|
242
268
|
// =============================================================================
|
|
243
269
|
|
|
244
270
|
class CallbackServer {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
async start(port, hostname, certDir, requestHandler) {
|
|
252
|
-
await this.stop();
|
|
253
|
-
|
|
254
|
-
return new Promise((resolve, reject) => {
|
|
255
|
-
const tryStart = (attempt = 1) => {
|
|
256
|
-
try {
|
|
257
|
-
const { key, cert } = SSLUtils.generateCert(hostname, certDir);
|
|
258
|
-
|
|
259
|
-
this.server = https.createServer({ key, cert }, requestHandler);
|
|
260
|
-
|
|
261
|
-
this.server.on('connection', (conn) => {
|
|
262
|
-
this.connections.add(conn);
|
|
263
|
-
conn.on('close', () => this.connections.delete(conn));
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
this.server.on('error', (err) => {
|
|
267
|
-
if (err.code === 'EADDRINUSE' && attempt < 3) {
|
|
268
|
-
console.warn(`Port ${port} in use, retrying in 1 second (attempt ${attempt}/3)...`);
|
|
269
|
-
setTimeout(() => tryStart(attempt + 1), 1000);
|
|
270
|
-
} else {
|
|
271
|
-
console.error('Callback server error:', err.message);
|
|
272
|
-
reject(err);
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
this.server.listen(port, '0.0.0.0', () => {
|
|
277
|
-
console.log(`HTTPS callback server listening on port ${port}`);
|
|
278
|
-
this.port = port;
|
|
279
|
-
resolve({ success: true, port });
|
|
280
|
-
});
|
|
281
|
-
} catch (error) {
|
|
282
|
-
reject(error);
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
tryStart();
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async stop() {
|
|
291
|
-
return new Promise((resolve) => {
|
|
292
|
-
if (!this.server) {
|
|
293
|
-
resolve({ success: true });
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
271
|
+
constructor() {
|
|
272
|
+
this.server = null;
|
|
273
|
+
this.port = null;
|
|
274
|
+
this.connections = new Set();
|
|
275
|
+
}
|
|
296
276
|
|
|
297
|
-
|
|
298
|
-
|
|
277
|
+
async start(port, hostname, certDir, requestHandler) {
|
|
278
|
+
await this.stop();
|
|
279
|
+
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
281
|
+
const tryStart = (attempt = 1) => {
|
|
282
|
+
try {
|
|
283
|
+
const { key, cert } = SSLUtils.generateCert(hostname, certDir);
|
|
284
|
+
|
|
285
|
+
this.server = https.createServer({ key, cert }, requestHandler);
|
|
286
|
+
|
|
287
|
+
this.server.on('connection', (conn) => {
|
|
288
|
+
this.connections.add(conn);
|
|
289
|
+
conn.on('close', () => this.connections.delete(conn));
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
this.server.on('error', (err) => {
|
|
293
|
+
if (err.code === 'EADDRINUSE' && attempt < 3) {
|
|
294
|
+
console.warn(`Port ${port} in use, retrying in 1 second (attempt ${attempt}/3)...`);
|
|
295
|
+
setTimeout(() => tryStart(attempt + 1), 1000);
|
|
296
|
+
} else {
|
|
297
|
+
console.error('Callback server error:', err.message);
|
|
298
|
+
reject(err);
|
|
299
299
|
}
|
|
300
|
-
|
|
300
|
+
});
|
|
301
301
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
302
|
+
this.server.listen(port, '0.0.0.0', () => {
|
|
303
|
+
console.log(`HTTPS callback server listening on port ${port}`);
|
|
304
|
+
this.port = port;
|
|
305
|
+
resolve({ success: true, port });
|
|
306
|
+
});
|
|
307
|
+
} catch (error) {
|
|
308
|
+
reject(error);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
308
311
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this.server = null;
|
|
313
|
-
this.port = null;
|
|
314
|
-
resolve({ success: true });
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
}
|
|
312
|
+
tryStart();
|
|
313
|
+
});
|
|
314
|
+
}
|
|
318
315
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
316
|
+
async stop() {
|
|
317
|
+
return new Promise((resolve) => {
|
|
318
|
+
if (!this.server) {
|
|
319
|
+
resolve({ success: true });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for (const conn of this.connections) {
|
|
324
|
+
conn.destroy();
|
|
325
|
+
}
|
|
326
|
+
this.connections.clear();
|
|
327
|
+
|
|
328
|
+
const timeout = setTimeout(() => {
|
|
329
|
+
console.warn('HTTPS callback server close timed out, forcing cleanup');
|
|
330
|
+
this.server = null;
|
|
331
|
+
this.port = null;
|
|
332
|
+
resolve({ success: true });
|
|
333
|
+
}, 2000);
|
|
334
|
+
|
|
335
|
+
this.server.close(() => {
|
|
336
|
+
clearTimeout(timeout);
|
|
337
|
+
console.log('HTTPS callback server stopped');
|
|
338
|
+
this.server = null;
|
|
339
|
+
this.port = null;
|
|
340
|
+
resolve({ success: true });
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
get isRunning() {
|
|
346
|
+
return this.server !== null;
|
|
347
|
+
}
|
|
322
348
|
}
|
|
323
349
|
|
|
324
350
|
// =============================================================================
|
|
@@ -326,12 +352,12 @@ class CallbackServer {
|
|
|
326
352
|
// =============================================================================
|
|
327
353
|
|
|
328
354
|
const HtmlTemplates = {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
355
|
+
callbackResponse(success, message) {
|
|
356
|
+
const icon = success
|
|
357
|
+
? '<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#4caf50" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>'
|
|
358
|
+
: '<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#f44336" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>';
|
|
333
359
|
|
|
334
|
-
|
|
360
|
+
return `<!DOCTYPE html>
|
|
335
361
|
<html>
|
|
336
362
|
<head>
|
|
337
363
|
<meta charset="UTF-8">
|
|
@@ -387,7 +413,7 @@ const HtmlTemplates = {
|
|
|
387
413
|
</div>
|
|
388
414
|
</body>
|
|
389
415
|
</html>`;
|
|
390
|
-
|
|
416
|
+
},
|
|
391
417
|
};
|
|
392
418
|
|
|
393
419
|
// =============================================================================
|
|
@@ -395,504 +421,522 @@ const HtmlTemplates = {
|
|
|
395
421
|
// =============================================================================
|
|
396
422
|
|
|
397
423
|
class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
424
|
+
constructor() {
|
|
425
|
+
super();
|
|
426
|
+
|
|
427
|
+
this.pendingAuth = null;
|
|
428
|
+
this.authResult = null;
|
|
429
|
+
this.callbackServer = new CallbackServer();
|
|
430
|
+
|
|
431
|
+
this.registerHandlers();
|
|
432
|
+
this.ready();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
getTokenFilePath() {
|
|
436
|
+
return resolve(this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '', '.daikin-controller-cloud-tokenset');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
getMobileTokenFilePath() {
|
|
440
|
+
return resolve(this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '', '.daikin-mobile-tokenset');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
getCertDir() {
|
|
444
|
+
return resolve(this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '', 'daikin-cloud-certs');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
getActiveTokenSet() {
|
|
448
|
+
// Check mobile token first (takes precedence)
|
|
449
|
+
const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
|
|
450
|
+
if (mobileTokenSet?.access_token) {
|
|
451
|
+
return mobileTokenSet;
|
|
407
452
|
}
|
|
408
|
-
|
|
409
|
-
getTokenFilePath()
|
|
410
|
-
|
|
453
|
+
// Fall back to developer portal token
|
|
454
|
+
return TokenManager.load(this.getTokenFilePath());
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
registerHandlers() {
|
|
458
|
+
this.onRequest('/auth/status', this.handleGetAuthStatus.bind(this));
|
|
459
|
+
this.onRequest('/auth/start', this.handleStartAuth.bind(this));
|
|
460
|
+
this.onRequest('/auth/', this.handleCallback.bind(this));
|
|
461
|
+
this.onRequest('/auth/revoke', this.handleRevokeAuth.bind(this));
|
|
462
|
+
this.onRequest('/auth/test', this.handleTestConnection.bind(this));
|
|
463
|
+
this.onRequest('/auth/poll', this.handlePollAuthResult.bind(this));
|
|
464
|
+
this.onRequest('/auth/stop-server', this.handleStopServer.bind(this));
|
|
465
|
+
this.onRequest('/auth/mobile-test', this.handleMobileAuthTest.bind(this));
|
|
466
|
+
this.onRequest('/config/validate', this.handleValidateConfig.bind(this));
|
|
467
|
+
this.onRequest('/devices/list', this.handleListDevices.bind(this));
|
|
468
|
+
this.onRequest('/api/rate-limit', this.handleGetRateLimit.bind(this));
|
|
469
|
+
this.onRequest('/server/info', this.handleGetServerInfo.bind(this));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// -------------------------------------------------------------------------
|
|
473
|
+
// Server Info Handler
|
|
474
|
+
// -------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
async handleGetServerInfo() {
|
|
477
|
+
const ipAddresses = this.getServerIpAddresses();
|
|
478
|
+
return {
|
|
479
|
+
ipAddresses,
|
|
480
|
+
primaryIp: ipAddresses[0] || null,
|
|
481
|
+
hostname: os.hostname(),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
getServerIpAddresses() {
|
|
486
|
+
const interfaces = os.networkInterfaces();
|
|
487
|
+
const addresses = [];
|
|
488
|
+
|
|
489
|
+
for (const nets of Object.values(interfaces)) {
|
|
490
|
+
if (!nets) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
for (const net of nets) {
|
|
494
|
+
// Skip internal and non-IPv4 addresses
|
|
495
|
+
if (net.internal || net.family !== 'IPv4') {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
addresses.push(net.address);
|
|
499
|
+
}
|
|
411
500
|
}
|
|
412
501
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
this.onRequest('/devices/list', this.handleListDevices.bind(this));
|
|
442
|
-
this.onRequest('/api/rate-limit', this.handleGetRateLimit.bind(this));
|
|
443
|
-
this.onRequest('/server/info', this.handleGetServerInfo.bind(this));
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// -------------------------------------------------------------------------
|
|
447
|
-
// Server Info Handler
|
|
448
|
-
// -------------------------------------------------------------------------
|
|
449
|
-
|
|
450
|
-
async handleGetServerInfo() {
|
|
451
|
-
const ipAddresses = this.getServerIpAddresses();
|
|
452
|
-
return {
|
|
453
|
-
ipAddresses,
|
|
454
|
-
primaryIp: ipAddresses[0] || null,
|
|
455
|
-
hostname: os.hostname(),
|
|
456
|
-
};
|
|
502
|
+
return addresses;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// -------------------------------------------------------------------------
|
|
506
|
+
// Auth Status Handler
|
|
507
|
+
// -------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
async handleGetAuthStatus() {
|
|
510
|
+
try {
|
|
511
|
+
// Check both token files - mobile takes precedence if exists
|
|
512
|
+
const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
|
|
513
|
+
const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
|
|
514
|
+
|
|
515
|
+
if (mobileTokenSet?.access_token) {
|
|
516
|
+
const status = TokenManager.getStatus(mobileTokenSet);
|
|
517
|
+
status.authMode = 'mobile_app';
|
|
518
|
+
return status;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (devPortalTokenSet?.access_token) {
|
|
522
|
+
const status = TokenManager.getStatus(devPortalTokenSet);
|
|
523
|
+
status.authMode = 'developer_portal';
|
|
524
|
+
return status;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return { authenticated: false, message: 'Not authenticated' };
|
|
528
|
+
} catch (error) {
|
|
529
|
+
return { authenticated: false, error: error.message, message: 'Error reading token status' };
|
|
457
530
|
}
|
|
531
|
+
}
|
|
458
532
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
533
|
+
// -------------------------------------------------------------------------
|
|
534
|
+
// Start Auth Handler
|
|
535
|
+
// -------------------------------------------------------------------------
|
|
462
536
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
for (const net of nets) {
|
|
466
|
-
// Skip internal and non-IPv4 addresses
|
|
467
|
-
if (net.internal || net.family !== 'IPv4') continue;
|
|
468
|
-
addresses.push(net.address);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
537
|
+
async handleStartAuth(payload) {
|
|
538
|
+
const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
|
|
471
539
|
|
|
472
|
-
|
|
540
|
+
if (!clientId || !clientSecret) {
|
|
541
|
+
throw new Error('Client ID and Client Secret are required');
|
|
473
542
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
// Auth Status Handler
|
|
477
|
-
// -------------------------------------------------------------------------
|
|
478
|
-
|
|
479
|
-
async handleGetAuthStatus() {
|
|
480
|
-
try {
|
|
481
|
-
// Check both token files - mobile takes precedence if exists
|
|
482
|
-
const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
|
|
483
|
-
const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
|
|
484
|
-
|
|
485
|
-
if (mobileTokenSet?.access_token) {
|
|
486
|
-
const status = TokenManager.getStatus(mobileTokenSet);
|
|
487
|
-
status.authMode = 'mobile_app';
|
|
488
|
-
return status;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (devPortalTokenSet?.access_token) {
|
|
492
|
-
const status = TokenManager.getStatus(devPortalTokenSet);
|
|
493
|
-
status.authMode = 'developer_portal';
|
|
494
|
-
return status;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return { authenticated: false, message: 'Not authenticated' };
|
|
498
|
-
} catch (error) {
|
|
499
|
-
return { authenticated: false, error: error.message, message: 'Error reading token status' };
|
|
500
|
-
}
|
|
543
|
+
if (!callbackServerExternalAddress) {
|
|
544
|
+
throw new Error('Callback Server Address is required');
|
|
501
545
|
}
|
|
502
546
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
547
|
+
const port = parseInt(callbackServerPort || '8582', 10);
|
|
548
|
+
const redirectUri = `https://${callbackServerExternalAddress}:${port}`;
|
|
549
|
+
const state = crypto.randomBytes(32).toString('hex');
|
|
550
|
+
|
|
551
|
+
this.pendingAuth = { state, clientId, clientSecret, redirectUri, port, createdAt: Date.now() };
|
|
552
|
+
this.authResult = null;
|
|
553
|
+
|
|
554
|
+
// Use static method from compiled src/api
|
|
555
|
+
const authUrl = DaikinOAuth.buildAuthUrlStatic(clientId, redirectUri, state);
|
|
556
|
+
console.log('[DaikinCloud] Generated auth URL:', authUrl);
|
|
557
|
+
console.log('[DaikinCloud] Redirect URI:', redirectUri);
|
|
558
|
+
|
|
559
|
+
// Try to start callback server for automatic code capture
|
|
560
|
+
let callbackServerRunning = false;
|
|
561
|
+
let callbackServerError = null;
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
await this.callbackServer.start(
|
|
565
|
+
port,
|
|
566
|
+
callbackServerExternalAddress,
|
|
567
|
+
this.getCertDir(),
|
|
568
|
+
this.handleHttpsCallback.bind(this),
|
|
569
|
+
);
|
|
570
|
+
callbackServerRunning = true;
|
|
571
|
+
console.log('[DaikinCloud] Callback server started successfully');
|
|
572
|
+
} catch (error) {
|
|
573
|
+
callbackServerError = error.message;
|
|
574
|
+
console.warn('[DaikinCloud] Failed to start callback server:', error.message);
|
|
575
|
+
}
|
|
516
576
|
|
|
517
|
-
|
|
518
|
-
|
|
577
|
+
return {
|
|
578
|
+
authUrl,
|
|
579
|
+
state,
|
|
580
|
+
redirectUri,
|
|
581
|
+
callbackServerRunning,
|
|
582
|
+
callbackServerError,
|
|
583
|
+
message: callbackServerRunning
|
|
584
|
+
? 'Callback server is running. Authentication will complete automatically.'
|
|
585
|
+
: 'Could not start callback server. After authenticating, copy the full callback URL and paste it below.',
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// -------------------------------------------------------------------------
|
|
590
|
+
// HTTPS Callback Handler
|
|
591
|
+
// -------------------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
handleHttpsCallback(req, res) {
|
|
594
|
+
const url = new URL(req.url, `https://${req.headers.host}`);
|
|
595
|
+
|
|
596
|
+
const code = url.searchParams.get('code');
|
|
597
|
+
const state = url.searchParams.get('state');
|
|
598
|
+
const error = url.searchParams.get('error');
|
|
599
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
600
|
+
|
|
601
|
+
if (error) {
|
|
602
|
+
this.authResult = { success: false, error: errorDescription || error };
|
|
603
|
+
this.sendCallbackResponse(res, false, errorDescription || error);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
519
606
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
607
|
+
if (!code || !state) {
|
|
608
|
+
this.authResult = { success: false, error: 'Missing code or state parameter' };
|
|
609
|
+
this.sendCallbackResponse(res, false, 'Missing authorization code');
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
524
612
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
613
|
+
if (!this.pendingAuth || state !== this.pendingAuth.state) {
|
|
614
|
+
this.authResult = { success: false, error: 'Invalid state parameter' };
|
|
615
|
+
this.sendCallbackResponse(res, false, 'Invalid state parameter');
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
528
618
|
|
|
529
|
-
|
|
530
|
-
await this.callbackServer.start(
|
|
531
|
-
port,
|
|
532
|
-
callbackServerExternalAddress,
|
|
533
|
-
this.getCertDir(),
|
|
534
|
-
this.handleHttpsCallback.bind(this)
|
|
535
|
-
);
|
|
536
|
-
callbackServerRunning = true;
|
|
537
|
-
console.log('[DaikinCloud] Callback server started successfully');
|
|
538
|
-
} catch (error) {
|
|
539
|
-
callbackServerError = error.message;
|
|
540
|
-
console.warn('[DaikinCloud] Failed to start callback server:', error.message);
|
|
541
|
-
}
|
|
619
|
+
const { clientId, clientSecret, redirectUri } = this.pendingAuth;
|
|
542
620
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
: 'Could not start callback server. After authenticating, copy the full callback URL and paste it below.',
|
|
621
|
+
// Use static method from compiled src/api
|
|
622
|
+
DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri)
|
|
623
|
+
.then((tokenSet) => {
|
|
624
|
+
TokenManager.save(this.getTokenFilePath(), tokenSet);
|
|
625
|
+
this.authResult = {
|
|
626
|
+
success: true,
|
|
627
|
+
message: 'Authentication successful!',
|
|
628
|
+
expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
|
|
552
629
|
};
|
|
630
|
+
this.pendingAuth = null;
|
|
631
|
+
this.sendCallbackResponse(res, true, 'Authentication successful! You can close this window.');
|
|
632
|
+
this.callbackServer.stop().catch(() => {});
|
|
633
|
+
})
|
|
634
|
+
.catch((err) => {
|
|
635
|
+
this.authResult = { success: false, error: err.message };
|
|
636
|
+
this.sendCallbackResponse(res, false, `Token exchange failed: ${err.message}`);
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
sendCallbackResponse(res, success, message) {
|
|
641
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
642
|
+
res.end(HtmlTemplates.callbackResponse(success, message));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// -------------------------------------------------------------------------
|
|
646
|
+
// Manual Callback Handler
|
|
647
|
+
// -------------------------------------------------------------------------
|
|
648
|
+
|
|
649
|
+
async handleCallback(payload) {
|
|
650
|
+
let { code, state, callbackUrl } = payload;
|
|
651
|
+
|
|
652
|
+
if (callbackUrl) {
|
|
653
|
+
try {
|
|
654
|
+
const url = new URL(callbackUrl);
|
|
655
|
+
code = url.searchParams.get('code');
|
|
656
|
+
state = url.searchParams.get('state');
|
|
657
|
+
} catch (e) {
|
|
658
|
+
throw new Error('Invalid callback URL format');
|
|
659
|
+
}
|
|
553
660
|
}
|
|
554
661
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (error) {
|
|
568
|
-
this.authResult = { success: false, error: errorDescription || error };
|
|
569
|
-
this.sendCallbackResponse(res, false, errorDescription || error);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (!code || !state) {
|
|
574
|
-
this.authResult = { success: false, error: 'Missing code or state parameter' };
|
|
575
|
-
this.sendCallbackResponse(res, false, 'Missing authorization code');
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if (!this.pendingAuth || state !== this.pendingAuth.state) {
|
|
580
|
-
this.authResult = { success: false, error: 'Invalid state parameter' };
|
|
581
|
-
this.sendCallbackResponse(res, false, 'Invalid state parameter');
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const { clientId, clientSecret, redirectUri } = this.pendingAuth;
|
|
586
|
-
|
|
587
|
-
// Use static method from compiled src/api
|
|
588
|
-
DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri)
|
|
589
|
-
.then((tokenSet) => {
|
|
590
|
-
TokenManager.save(this.getTokenFilePath(), tokenSet);
|
|
591
|
-
this.authResult = {
|
|
592
|
-
success: true,
|
|
593
|
-
message: 'Authentication successful!',
|
|
594
|
-
expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
|
|
595
|
-
};
|
|
596
|
-
this.pendingAuth = null;
|
|
597
|
-
this.sendCallbackResponse(res, true, 'Authentication successful! You can close this window.');
|
|
598
|
-
this.callbackServer.stop().catch(() => {});
|
|
599
|
-
})
|
|
600
|
-
.catch((err) => {
|
|
601
|
-
this.authResult = { success: false, error: err.message };
|
|
602
|
-
this.sendCallbackResponse(res, false, `Token exchange failed: ${err.message}`);
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
sendCallbackResponse(res, success, message) {
|
|
607
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
608
|
-
res.end(HtmlTemplates.callbackResponse(success, message));
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// -------------------------------------------------------------------------
|
|
612
|
-
// Manual Callback Handler
|
|
613
|
-
// -------------------------------------------------------------------------
|
|
614
|
-
|
|
615
|
-
async handleCallback(payload) {
|
|
616
|
-
let { code, state, callbackUrl } = payload;
|
|
617
|
-
|
|
618
|
-
if (callbackUrl) {
|
|
619
|
-
try {
|
|
620
|
-
const url = new URL(callbackUrl);
|
|
621
|
-
code = url.searchParams.get('code');
|
|
622
|
-
state = url.searchParams.get('state');
|
|
623
|
-
} catch (e) {
|
|
624
|
-
throw new Error('Invalid callback URL format');
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
if (!code) throw new Error('Authorization code is required');
|
|
629
|
-
if (!this.pendingAuth) throw new Error('No pending authorization. Please start the auth flow again.');
|
|
630
|
-
if (state && state !== this.pendingAuth.state) throw new Error('Invalid state parameter. Please try again.');
|
|
631
|
-
if (Date.now() - this.pendingAuth.createdAt > 10 * 60 * 1000) {
|
|
632
|
-
this.pendingAuth = null;
|
|
633
|
-
throw new Error('Authorization request expired. Please try again.');
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const { clientId, clientSecret, redirectUri } = this.pendingAuth;
|
|
637
|
-
|
|
638
|
-
try {
|
|
639
|
-
// Use static method from compiled src/api
|
|
640
|
-
const tokenSet = await DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri);
|
|
641
|
-
TokenManager.save(this.getTokenFilePath(), tokenSet);
|
|
642
|
-
this.pendingAuth = null;
|
|
643
|
-
|
|
644
|
-
await this.callbackServer.stop();
|
|
645
|
-
|
|
646
|
-
return {
|
|
647
|
-
success: true,
|
|
648
|
-
message: 'Authentication successful! Restart Homebridge to apply.',
|
|
649
|
-
expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
|
|
650
|
-
};
|
|
651
|
-
} catch (error) {
|
|
652
|
-
throw new Error(`Token exchange failed: ${error.message}`);
|
|
653
|
-
}
|
|
662
|
+
if (!code) {
|
|
663
|
+
throw new Error('Authorization code is required');
|
|
664
|
+
}
|
|
665
|
+
if (!this.pendingAuth) {
|
|
666
|
+
throw new Error('No pending authorization. Please start the auth flow again.');
|
|
667
|
+
}
|
|
668
|
+
if (state && state !== this.pendingAuth.state) {
|
|
669
|
+
throw new Error('Invalid state parameter. Please try again.');
|
|
670
|
+
}
|
|
671
|
+
if (Date.now() - this.pendingAuth.createdAt > 10 * 60 * 1000) {
|
|
672
|
+
this.pendingAuth = null;
|
|
673
|
+
throw new Error('Authorization request expired. Please try again.');
|
|
654
674
|
}
|
|
655
675
|
|
|
656
|
-
|
|
657
|
-
// Poll Auth Result Handler
|
|
658
|
-
// -------------------------------------------------------------------------
|
|
676
|
+
const { clientId, clientSecret, redirectUri } = this.pendingAuth;
|
|
659
677
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
await this.callbackServer.stop();
|
|
666
|
-
}
|
|
667
|
-
return result;
|
|
668
|
-
}
|
|
678
|
+
try {
|
|
679
|
+
// Use static method from compiled src/api
|
|
680
|
+
const tokenSet = await DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri);
|
|
681
|
+
TokenManager.save(this.getTokenFilePath(), tokenSet);
|
|
682
|
+
this.pendingAuth = null;
|
|
669
683
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
684
|
+
await this.callbackServer.stop();
|
|
685
|
+
|
|
686
|
+
return {
|
|
687
|
+
success: true,
|
|
688
|
+
message: 'Authentication successful! Restart Homebridge to apply.',
|
|
689
|
+
expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
|
|
690
|
+
};
|
|
691
|
+
} catch (error) {
|
|
692
|
+
throw new Error(`Token exchange failed: ${error.message}`);
|
|
679
693
|
}
|
|
694
|
+
}
|
|
680
695
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
696
|
+
// -------------------------------------------------------------------------
|
|
697
|
+
// Poll Auth Result Handler
|
|
698
|
+
// -------------------------------------------------------------------------
|
|
684
699
|
|
|
685
|
-
|
|
686
|
-
|
|
700
|
+
async handlePollAuthResult() {
|
|
701
|
+
if (this.authResult) {
|
|
702
|
+
const result = { ...this.authResult };
|
|
703
|
+
if (result.success) {
|
|
687
704
|
this.authResult = null;
|
|
688
705
|
await this.callbackServer.stop();
|
|
689
|
-
|
|
706
|
+
}
|
|
707
|
+
return result;
|
|
690
708
|
}
|
|
691
709
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (!email || !password) {
|
|
700
|
-
return { success: false, message: 'Email and password are required' };
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const tokenFilePath = this.getMobileTokenFilePath();
|
|
704
|
-
|
|
705
|
-
try {
|
|
706
|
-
// Create a temporary mobile OAuth client
|
|
707
|
-
const mobileOAuth = new DaikinMobileOAuth({
|
|
708
|
-
email,
|
|
709
|
-
password,
|
|
710
|
-
tokenFilePath,
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
// Perform authentication
|
|
714
|
-
console.log('[DaikinCloud] Testing mobile app authentication...');
|
|
715
|
-
const tokenSet = await mobileOAuth.authenticate();
|
|
716
|
-
console.log('[DaikinCloud] Mobile authentication successful');
|
|
717
|
-
|
|
718
|
-
// Test API access and get device count
|
|
719
|
-
let deviceCount = 0;
|
|
720
|
-
let rateLimit = null;
|
|
721
|
-
|
|
722
|
-
try {
|
|
723
|
-
const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
|
|
724
|
-
deviceCount = Array.isArray(result.data) ? result.data.length : 0;
|
|
725
|
-
rateLimit = result.rateLimit;
|
|
726
|
-
} catch (apiError) {
|
|
727
|
-
console.warn('[DaikinCloud] API test failed:', apiError.message);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
return {
|
|
731
|
-
success: true,
|
|
732
|
-
message: 'Authentication successful!',
|
|
733
|
-
deviceCount,
|
|
734
|
-
rateLimit,
|
|
735
|
-
expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
|
|
736
|
-
};
|
|
737
|
-
} catch (error) {
|
|
738
|
-
console.error('[DaikinCloud] Mobile auth test failed:', error.message);
|
|
739
|
-
return {
|
|
740
|
-
success: false,
|
|
741
|
-
message: error.message || 'Authentication failed',
|
|
742
|
-
};
|
|
743
|
-
}
|
|
710
|
+
const tokenSet = TokenManager.load(this.getTokenFilePath());
|
|
711
|
+
if (tokenSet && tokenSet.access_token) {
|
|
712
|
+
return {
|
|
713
|
+
success: true,
|
|
714
|
+
message: 'Authentication successful!',
|
|
715
|
+
expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
|
|
716
|
+
};
|
|
744
717
|
}
|
|
718
|
+
return { pending: true };
|
|
719
|
+
}
|
|
745
720
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
721
|
+
// -------------------------------------------------------------------------
|
|
722
|
+
// Stop Server Handler
|
|
723
|
+
// -------------------------------------------------------------------------
|
|
749
724
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
725
|
+
async handleStopServer() {
|
|
726
|
+
this.pendingAuth = null;
|
|
727
|
+
this.authResult = null;
|
|
728
|
+
await this.callbackServer.stop();
|
|
729
|
+
return { success: true };
|
|
730
|
+
}
|
|
753
731
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
732
|
+
// -------------------------------------------------------------------------
|
|
733
|
+
// Mobile Auth Test Handler
|
|
734
|
+
// -------------------------------------------------------------------------
|
|
757
735
|
|
|
758
|
-
|
|
736
|
+
async handleMobileAuthTest(payload) {
|
|
737
|
+
const { email, password } = payload;
|
|
759
738
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
await DaikinOAuth.revokeTokenStatic(devPortalTokenSet.refresh_token, clientId, clientSecret);
|
|
764
|
-
} catch (error) {
|
|
765
|
-
console.warn('Failed to revoke token at server:', error.message);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
739
|
+
if (!email || !password) {
|
|
740
|
+
return { success: false, message: 'Email and password are required' };
|
|
741
|
+
}
|
|
768
742
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
743
|
+
const tokenFilePath = this.getMobileTokenFilePath();
|
|
744
|
+
|
|
745
|
+
try {
|
|
746
|
+
// Create a temporary mobile OAuth client
|
|
747
|
+
const mobileOAuth = new DaikinMobileOAuth({
|
|
748
|
+
email,
|
|
749
|
+
password,
|
|
750
|
+
tokenFilePath,
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Perform authentication
|
|
754
|
+
console.log('[DaikinCloud] Testing mobile app authentication...');
|
|
755
|
+
const tokenSet = await mobileOAuth.authenticate();
|
|
756
|
+
console.log('[DaikinCloud] Mobile authentication successful');
|
|
757
|
+
|
|
758
|
+
// Test API access and get device count
|
|
759
|
+
let deviceCount = 0;
|
|
760
|
+
let rateLimit = null;
|
|
761
|
+
|
|
762
|
+
try {
|
|
763
|
+
const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
|
|
764
|
+
deviceCount = Array.isArray(result.data) ? result.data.length : 0;
|
|
765
|
+
rateLimit = result.rateLimit;
|
|
766
|
+
} catch (apiError) {
|
|
767
|
+
console.warn('[DaikinCloud] API test failed:', apiError.message);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
success: true,
|
|
772
|
+
message: 'Authentication successful!',
|
|
773
|
+
deviceCount,
|
|
774
|
+
rateLimit,
|
|
775
|
+
expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
|
|
776
|
+
};
|
|
777
|
+
} catch (error) {
|
|
778
|
+
console.error('[DaikinCloud] Mobile auth test failed:', error.message);
|
|
779
|
+
return {
|
|
780
|
+
success: false,
|
|
781
|
+
message: error.message || 'Authentication failed',
|
|
782
|
+
};
|
|
773
783
|
}
|
|
784
|
+
}
|
|
774
785
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
786
|
+
// -------------------------------------------------------------------------
|
|
787
|
+
// Revoke Auth Handler
|
|
788
|
+
// -------------------------------------------------------------------------
|
|
778
789
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
return { success: false, message: 'Not authenticated. Please authenticate first.' };
|
|
783
|
-
}
|
|
790
|
+
async handleRevokeAuth(payload) {
|
|
791
|
+
const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
|
|
792
|
+
const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
|
|
784
793
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
|
|
788
|
-
const devices = result.data;
|
|
789
|
-
return {
|
|
790
|
-
success: true,
|
|
791
|
-
message: `Connection successful! Found ${Array.isArray(devices) ? devices.length : 0} device(s).`,
|
|
792
|
-
deviceCount: Array.isArray(devices) ? devices.length : 0,
|
|
793
|
-
};
|
|
794
|
-
} catch (error) {
|
|
795
|
-
return { success: false, message: `Connection failed: ${error.message}`, error: error.message };
|
|
796
|
-
}
|
|
794
|
+
if (!devPortalTokenSet && !mobileTokenSet) {
|
|
795
|
+
return { success: true, message: 'No tokens to revoke' };
|
|
797
796
|
}
|
|
798
797
|
|
|
799
|
-
|
|
800
|
-
// List Devices Handler
|
|
801
|
-
// -------------------------------------------------------------------------
|
|
798
|
+
const { clientId, clientSecret } = payload;
|
|
802
799
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
tokenSet = TokenManager.load(this.getTokenFilePath());
|
|
812
|
-
} else {
|
|
813
|
-
tokenSet = this.getActiveTokenSet();
|
|
814
|
-
}
|
|
800
|
+
if (devPortalTokenSet?.refresh_token && clientId && clientSecret) {
|
|
801
|
+
try {
|
|
802
|
+
// Use static method from compiled src/api
|
|
803
|
+
await DaikinOAuth.revokeTokenStatic(devPortalTokenSet.refresh_token, clientId, clientSecret);
|
|
804
|
+
} catch (error) {
|
|
805
|
+
console.warn('Failed to revoke token at server:', error.message);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
815
808
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
809
|
+
// Delete both token files
|
|
810
|
+
TokenManager.delete(this.getTokenFilePath());
|
|
811
|
+
TokenManager.delete(this.getMobileTokenFilePath());
|
|
812
|
+
return { success: true, message: 'Authentication revoked. You will need to re-authenticate.' };
|
|
813
|
+
}
|
|
820
814
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
815
|
+
// -------------------------------------------------------------------------
|
|
816
|
+
// Test Connection Handler
|
|
817
|
+
// -------------------------------------------------------------------------
|
|
824
818
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const devices = Array.isArray(gatewayDevices)
|
|
830
|
-
? gatewayDevices.map(device => DeviceExtractor.extractAll(device))
|
|
831
|
-
: [];
|
|
832
|
-
|
|
833
|
-
return { success: true, devices, message: `Found ${devices.length} device(s).` };
|
|
834
|
-
} catch (error) {
|
|
835
|
-
return { success: false, devices: [], message: `Failed to fetch devices: ${error.message}`, error: error.message };
|
|
836
|
-
}
|
|
819
|
+
async handleTestConnection() {
|
|
820
|
+
const tokenSet = this.getActiveTokenSet();
|
|
821
|
+
if (!tokenSet?.access_token) {
|
|
822
|
+
return { success: false, message: 'Not authenticated. Please authenticate first.' };
|
|
837
823
|
}
|
|
838
824
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
825
|
+
try {
|
|
826
|
+
// Use static method from compiled src/api
|
|
827
|
+
const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
|
|
828
|
+
const devices = result.data;
|
|
829
|
+
return {
|
|
830
|
+
success: true,
|
|
831
|
+
message: `Connection successful! Found ${Array.isArray(devices) ? devices.length : 0} device(s).`,
|
|
832
|
+
deviceCount: Array.isArray(devices) ? devices.length : 0,
|
|
833
|
+
};
|
|
834
|
+
} catch (error) {
|
|
835
|
+
return { success: false, message: `Connection failed: ${error.message}`, error: error.message };
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// -------------------------------------------------------------------------
|
|
840
|
+
// List Devices Handler
|
|
841
|
+
// -------------------------------------------------------------------------
|
|
842
|
+
|
|
843
|
+
async handleListDevices(payload) {
|
|
844
|
+
const mode = payload?.mode;
|
|
845
|
+
|
|
846
|
+
// Get token based on mode parameter, or fall back to active token
|
|
847
|
+
let tokenSet;
|
|
848
|
+
if (mode === 'mobile_app') {
|
|
849
|
+
tokenSet = TokenManager.load(this.getMobileTokenFilePath());
|
|
850
|
+
} else if (mode === 'developer_portal') {
|
|
851
|
+
tokenSet = TokenManager.load(this.getTokenFilePath());
|
|
852
|
+
} else {
|
|
853
|
+
tokenSet = this.getActiveTokenSet();
|
|
854
|
+
}
|
|
842
855
|
|
|
843
|
-
|
|
844
|
-
|
|
856
|
+
// If the requested mode's token doesn't exist or is invalid, fall back to active token
|
|
857
|
+
if (!tokenSet?.access_token) {
|
|
858
|
+
tokenSet = this.getActiveTokenSet();
|
|
859
|
+
}
|
|
845
860
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
tokenSet = TokenManager.load(this.getMobileTokenFilePath());
|
|
850
|
-
} else if (mode === 'developer_portal') {
|
|
851
|
-
tokenSet = TokenManager.load(this.getTokenFilePath());
|
|
852
|
-
} else {
|
|
853
|
-
tokenSet = this.getActiveTokenSet();
|
|
854
|
-
}
|
|
861
|
+
if (!tokenSet?.access_token) {
|
|
862
|
+
return { success: false, devices: [], message: 'Not authenticated. Please authenticate first.' };
|
|
863
|
+
}
|
|
855
864
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
865
|
+
try {
|
|
866
|
+
// Use static method from compiled src/api
|
|
867
|
+
const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
|
|
868
|
+
const gatewayDevices = result.data;
|
|
869
|
+
const devices = Array.isArray(gatewayDevices)
|
|
870
|
+
? gatewayDevices.map(device => DeviceExtractor.extractAll(device))
|
|
871
|
+
: [];
|
|
872
|
+
|
|
873
|
+
return { success: true, devices, message: `Found ${devices.length} device(s).` };
|
|
874
|
+
} catch (error) {
|
|
875
|
+
return { success: false, devices: [], message: `Failed to fetch devices: ${error.message}`, error: error.message };
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// -------------------------------------------------------------------------
|
|
880
|
+
// Get Rate Limit Handler
|
|
881
|
+
// -------------------------------------------------------------------------
|
|
882
|
+
|
|
883
|
+
async handleGetRateLimit(payload) {
|
|
884
|
+
const mode = payload?.mode;
|
|
885
|
+
|
|
886
|
+
// Get token based on mode parameter, or fall back to active token
|
|
887
|
+
let tokenSet;
|
|
888
|
+
if (mode === 'mobile_app') {
|
|
889
|
+
tokenSet = TokenManager.load(this.getMobileTokenFilePath());
|
|
890
|
+
} else if (mode === 'developer_portal') {
|
|
891
|
+
tokenSet = TokenManager.load(this.getTokenFilePath());
|
|
892
|
+
} else {
|
|
893
|
+
tokenSet = this.getActiveTokenSet();
|
|
894
|
+
}
|
|
859
895
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
|
|
863
|
-
return { success: true, rateLimit: result.rateLimit };
|
|
864
|
-
} catch (error) {
|
|
865
|
-
return { success: false, message: error.message };
|
|
866
|
-
}
|
|
896
|
+
if (!tokenSet?.access_token) {
|
|
897
|
+
return { success: false, message: 'Not authenticated' };
|
|
867
898
|
}
|
|
868
899
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
900
|
+
try {
|
|
901
|
+
// Use static method from compiled src/api
|
|
902
|
+
const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
|
|
903
|
+
return { success: true, rateLimit: result.rateLimit };
|
|
904
|
+
} catch (error) {
|
|
905
|
+
return { success: false, message: error.message };
|
|
906
|
+
}
|
|
907
|
+
}
|
|
872
908
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
|
|
909
|
+
// -------------------------------------------------------------------------
|
|
910
|
+
// Validate Config Handler
|
|
911
|
+
// -------------------------------------------------------------------------
|
|
877
912
|
|
|
878
|
-
|
|
879
|
-
|
|
913
|
+
async handleValidateConfig(payload) {
|
|
914
|
+
const errors = [];
|
|
915
|
+
const warnings = [];
|
|
916
|
+
const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
|
|
880
917
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
918
|
+
if (!clientId) {
|
|
919
|
+
errors.push('Client ID is required. Get it from the Daikin Developer Portal.');
|
|
920
|
+
}
|
|
921
|
+
if (!clientSecret) {
|
|
922
|
+
errors.push('Client Secret is required. Get it from the Daikin Developer Portal.');
|
|
923
|
+
}
|
|
886
924
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
925
|
+
if (!callbackServerExternalAddress) {
|
|
926
|
+
errors.push('Callback Server External Address is required.');
|
|
927
|
+
} else if (callbackServerExternalAddress === 'localhost' || callbackServerExternalAddress === '127.0.0.1') {
|
|
928
|
+
errors.push('Callback address cannot be localhost. Use your external IP or domain.');
|
|
929
|
+
}
|
|
893
930
|
|
|
894
|
-
|
|
931
|
+
const port = parseInt(callbackServerPort || '8582', 10);
|
|
932
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
933
|
+
errors.push('Invalid port number. Must be between 1 and 65535.');
|
|
934
|
+
} else if (port < 1024) {
|
|
935
|
+
warnings.push('Using a privileged port (< 1024) may require root permissions.');
|
|
895
936
|
}
|
|
937
|
+
|
|
938
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
939
|
+
}
|
|
896
940
|
}
|
|
897
941
|
|
|
898
942
|
// =============================================================================
|