@mp-consulting/homebridge-daikin-cloud 1.6.0 → 1.6.1

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.
@@ -1,4 +1,3 @@
1
- const { HomebridgePluginUiServer } = require('@homebridge/plugin-ui-utils');
2
1
  const { resolve, join } = require('path');
3
2
  const fs = require('fs');
4
3
  const https = require('https');
@@ -504,645 +503,676 @@ const HtmlTemplates = {
504
503
  // Main Server Class
505
504
  // =============================================================================
506
505
 
507
- class DaikinCloudUiServer extends HomebridgePluginUiServer {
508
- constructor() {
509
- super();
506
+ /**
507
+ * Build the UI server class on top of HomebridgePluginUiServer. The base is
508
+ * passed in because @homebridge/plugin-ui-utils is ESM-only: require() loads it
509
+ * on Node >= 20.19 / 22.12, older runtimes need import() (see loadUiUtils).
510
+ */
511
+ function createDaikinCloudUiServer(HomebridgePluginUiServer) {
512
+ return class DaikinCloudUiServer extends HomebridgePluginUiServer {
513
+ constructor() {
514
+ super();
510
515
 
511
- this.pendingAuth = null;
512
- this.authResult = null;
513
- this.callbackServer = new CallbackServer();
514
- this.devicesCache = new Map();
516
+ this.pendingAuth = null;
517
+ this.authResult = null;
518
+ this.callbackServer = new CallbackServer();
519
+ this.devicesCache = new Map();
515
520
 
516
- this.applyTransportFromConfig();
517
- this.registerHandlers();
518
- this.ready();
519
- }
521
+ this.applyTransportFromConfig();
522
+ this.registerHandlers();
523
+ this.ready();
524
+ }
520
525
 
521
- getStoragePath() {
522
- return this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '';
523
- }
526
+ getStoragePath() {
527
+ return this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '';
528
+ }
524
529
 
525
- /**
530
+ /**
526
531
  * Read the DaikinCloud platform block from the Homebridge config.json.
527
532
  * Throws if the config file cannot be read or parsed.
528
533
  */
529
- readPlatformConfig() {
530
- const configPath = this.homebridgeConfigPath || resolve(this.getStoragePath(), 'config.json');
531
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
532
- return (config.platforms || []).find((p) => p.platform === 'DaikinCloud') || null;
533
- }
534
+ readPlatformConfig() {
535
+ const configPath = this.homebridgeConfigPath || resolve(this.getStoragePath(), 'config.json');
536
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
537
+ return (config.platforms || []).find((p) => p.platform === 'DaikinCloud') || null;
538
+ }
534
539
 
535
- /**
540
+ /**
536
541
  * Honour the plugin's httpTransport setting in the setup wizard too, so a
537
542
  * network that blocks Node's TLS fingerprint (issue #6) can still complete
538
543
  * authentication from the UI. Env var DAIKIN_HTTP_TRANSPORT wins.
539
544
  */
540
- applyTransportFromConfig() {
541
- try {
542
- const platform = this.readPlatformConfig();
543
- configureHttpTransport(platform && platform.httpTransport);
544
- } catch (e) {
545
- console.log('[DaikinCloud] Could not read httpTransport from config, using default:', e.message);
545
+ applyTransportFromConfig() {
546
+ try {
547
+ const platform = this.readPlatformConfig();
548
+ configureHttpTransport(platform && platform.httpTransport);
549
+ } catch (e) {
550
+ console.log('[DaikinCloud] Could not read httpTransport from config, using default:', e.message);
551
+ }
546
552
  }
547
- }
548
553
 
549
- /**
554
+ /**
550
555
  * Resolve the callback server bind address from the request payload, then the
551
556
  * saved platform config, falling back to DEFAULT_CALLBACK_BIND_ADDR when the
552
557
  * value is missing or not an IPv4/IPv6 literal.
553
558
  */
554
- getCallbackBindAddress(payload) {
555
- let configured = payload?.oidcCallbackServerBindAddr;
556
- if (configured === undefined || configured === null || configured === '') {
557
- try {
558
- configured = this.readPlatformConfig()?.oidcCallbackServerBindAddr;
559
- } catch (e) {
560
- configured = undefined;
559
+ getCallbackBindAddress(payload) {
560
+ let configured = payload?.oidcCallbackServerBindAddr;
561
+ if (configured === undefined || configured === null || configured === '') {
562
+ try {
563
+ configured = this.readPlatformConfig()?.oidcCallbackServerBindAddr;
564
+ } catch (e) {
565
+ configured = undefined;
566
+ }
561
567
  }
562
- }
563
568
 
564
- const bindAddr = NetUtils.resolveBindAddress(configured);
565
- if (configured && bindAddr !== String(configured).trim()) {
566
- console.warn(`[DaikinCloud] Invalid callback bind address "${configured}", using ${bindAddr}`);
569
+ const bindAddr = NetUtils.resolveBindAddress(configured);
570
+ if (configured && bindAddr !== String(configured).trim()) {
571
+ console.warn(`[DaikinCloud] Invalid callback bind address "${configured}", using ${bindAddr}`);
572
+ }
573
+ return bindAddr;
567
574
  }
568
- return bindAddr;
569
- }
570
575
 
571
- getTokenFilePath() {
572
- return resolve(this.getStoragePath(), TOKEN_FILES.developer_portal);
573
- }
576
+ getTokenFilePath() {
577
+ return resolve(this.getStoragePath(), TOKEN_FILES.developer_portal);
578
+ }
574
579
 
575
- getMobileTokenFilePath() {
576
- return resolve(this.getStoragePath(), TOKEN_FILES.mobile_app);
577
- }
580
+ getMobileTokenFilePath() {
581
+ return resolve(this.getStoragePath(), TOKEN_FILES.mobile_app);
582
+ }
578
583
 
579
- getCertDir() {
580
- return resolve(this.getStoragePath(), 'daikin-cloud-certs');
581
- }
584
+ getCertDir() {
585
+ return resolve(this.getStoragePath(), 'daikin-cloud-certs');
586
+ }
582
587
 
583
- getRateLimitFilePath() {
584
- return resolve(this.getStoragePath(), RATE_LIMIT_STATUS_FILE);
585
- }
588
+ getRateLimitFilePath() {
589
+ return resolve(this.getStoragePath(), RATE_LIMIT_STATUS_FILE);
590
+ }
586
591
 
587
- /**
592
+ /**
588
593
  * Active token set and the auth mode it belongs to (mobile takes precedence).
589
594
  */
590
- getActiveToken() {
591
- const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
592
- if (mobileTokenSet?.access_token) {
593
- return { tokenSet: mobileTokenSet, mode: 'mobile_app' };
595
+ getActiveToken() {
596
+ const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
597
+ if (mobileTokenSet?.access_token) {
598
+ return { tokenSet: mobileTokenSet, mode: 'mobile_app' };
599
+ }
600
+ return { tokenSet: TokenManager.load(this.getTokenFilePath()), mode: 'developer_portal' };
594
601
  }
595
- return { tokenSet: TokenManager.load(this.getTokenFilePath()), mode: 'developer_portal' };
596
- }
597
602
 
598
- getActiveTokenSet() {
599
- return this.getActiveToken().tokenSet;
600
- }
603
+ getActiveTokenSet() {
604
+ return this.getActiveToken().tokenSet;
605
+ }
601
606
 
602
- /**
607
+ /**
603
608
  * Token set for the requested mode, falling back to the active token set.
604
609
  */
605
- resolveToken(mode) {
606
- let tokenSet = null;
607
- if (mode === 'mobile_app') {
608
- tokenSet = TokenManager.load(this.getMobileTokenFilePath());
609
- } else if (mode === 'developer_portal') {
610
- tokenSet = TokenManager.load(this.getTokenFilePath());
611
- }
610
+ resolveToken(mode) {
611
+ let tokenSet = null;
612
+ if (mode === 'mobile_app') {
613
+ tokenSet = TokenManager.load(this.getMobileTokenFilePath());
614
+ } else if (mode === 'developer_portal') {
615
+ tokenSet = TokenManager.load(this.getTokenFilePath());
616
+ }
612
617
 
613
- if (tokenSet?.access_token) {
614
- return { tokenSet, mode };
618
+ if (tokenSet?.access_token) {
619
+ return { tokenSet, mode };
620
+ }
621
+ return this.getActiveToken();
615
622
  }
616
- return this.getActiveToken();
617
- }
618
623
 
619
- invalidateDevicesCache() {
620
- this.devicesCache.clear();
621
- }
624
+ invalidateDevicesCache() {
625
+ this.devicesCache.clear();
626
+ }
622
627
 
623
- isPendingAuthExpired() {
624
- return !!this.pendingAuth && Date.now() - this.pendingAuth.createdAt > PENDING_AUTH_TTL_MS;
625
- }
628
+ isPendingAuthExpired() {
629
+ return !!this.pendingAuth && Date.now() - this.pendingAuth.createdAt > PENDING_AUTH_TTL_MS;
630
+ }
626
631
 
627
- registerHandlers() {
628
- this.onRequest('/auth/status', this.handleGetAuthStatus.bind(this));
629
- this.onRequest('/auth/start', this.handleStartAuth.bind(this));
630
- this.onRequest('/auth/', this.handleCallback.bind(this));
631
- this.onRequest('/auth/revoke', this.handleRevokeAuth.bind(this));
632
- this.onRequest('/auth/test', this.handleTestConnection.bind(this));
633
- this.onRequest('/auth/poll', this.handlePollAuthResult.bind(this));
634
- this.onRequest('/auth/stop-server', this.handleStopServer.bind(this));
635
- this.onRequest('/auth/mobile-test', this.handleMobileAuthTest.bind(this));
636
- this.onRequest('/config/validate', this.handleValidateConfig.bind(this));
637
- this.onRequest('/devices/list', this.handleListDevices.bind(this));
638
- this.onRequest('/api/rate-limit', this.handleGetRateLimit.bind(this));
639
- this.onRequest('/server/info', this.handleGetServerInfo.bind(this));
640
- }
632
+ registerHandlers() {
633
+ this.onRequest('/auth/status', this.handleGetAuthStatus.bind(this));
634
+ this.onRequest('/auth/start', this.handleStartAuth.bind(this));
635
+ this.onRequest('/auth/', this.handleCallback.bind(this));
636
+ this.onRequest('/auth/revoke', this.handleRevokeAuth.bind(this));
637
+ this.onRequest('/auth/test', this.handleTestConnection.bind(this));
638
+ this.onRequest('/auth/poll', this.handlePollAuthResult.bind(this));
639
+ this.onRequest('/auth/stop-server', this.handleStopServer.bind(this));
640
+ this.onRequest('/auth/mobile-test', this.handleMobileAuthTest.bind(this));
641
+ this.onRequest('/config/validate', this.handleValidateConfig.bind(this));
642
+ this.onRequest('/devices/list', this.handleListDevices.bind(this));
643
+ this.onRequest('/api/rate-limit', this.handleGetRateLimit.bind(this));
644
+ this.onRequest('/server/info', this.handleGetServerInfo.bind(this));
645
+ }
641
646
 
642
- // -------------------------------------------------------------------------
643
- // Server Info Handler
644
- // -------------------------------------------------------------------------
647
+ // -------------------------------------------------------------------------
648
+ // Server Info Handler
649
+ // -------------------------------------------------------------------------
645
650
 
646
- async handleGetServerInfo() {
647
- const ipAddresses = this.getServerIpAddresses();
648
- return {
649
- ipAddresses,
650
- primaryIp: ipAddresses[0] || null,
651
- hostname: os.hostname(),
652
- };
653
- }
651
+ async handleGetServerInfo() {
652
+ const ipAddresses = this.getServerIpAddresses();
653
+ return {
654
+ ipAddresses,
655
+ primaryIp: ipAddresses[0] || null,
656
+ hostname: os.hostname(),
657
+ };
658
+ }
654
659
 
655
- getServerIpAddresses() {
656
- const interfaces = os.networkInterfaces();
657
- const addresses = [];
660
+ getServerIpAddresses() {
661
+ const interfaces = os.networkInterfaces();
662
+ const addresses = [];
658
663
 
659
- for (const nets of Object.values(interfaces)) {
660
- if (!nets) {
661
- continue;
662
- }
663
- for (const net of nets) {
664
- // Skip internal and non-IPv4 addresses
665
- if (net.internal || net.family !== 'IPv4') {
664
+ for (const nets of Object.values(interfaces)) {
665
+ if (!nets) {
666
666
  continue;
667
667
  }
668
- addresses.push(net.address);
668
+ for (const net of nets) {
669
+ // Skip internal and non-IPv4 addresses
670
+ if (net.internal || net.family !== 'IPv4') {
671
+ continue;
672
+ }
673
+ addresses.push(net.address);
674
+ }
669
675
  }
670
- }
671
676
 
672
- return addresses;
673
- }
677
+ return addresses;
678
+ }
674
679
 
675
- // -------------------------------------------------------------------------
676
- // Auth Status Handler
677
- // -------------------------------------------------------------------------
680
+ // -------------------------------------------------------------------------
681
+ // Auth Status Handler
682
+ // -------------------------------------------------------------------------
678
683
 
679
- async handleGetAuthStatus() {
680
- try {
684
+ async handleGetAuthStatus() {
685
+ try {
681
686
  // Check both token files - mobile takes precedence if exists
682
- const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
683
- const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
687
+ const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
688
+ const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
684
689
 
685
- if (mobileTokenSet?.access_token) {
686
- const status = TokenManager.getStatus(mobileTokenSet);
687
- status.authMode = 'mobile_app';
688
- return status;
689
- }
690
+ if (mobileTokenSet?.access_token) {
691
+ const status = TokenManager.getStatus(mobileTokenSet);
692
+ status.authMode = 'mobile_app';
693
+ return status;
694
+ }
690
695
 
691
- if (devPortalTokenSet?.access_token) {
692
- const status = TokenManager.getStatus(devPortalTokenSet);
693
- status.authMode = 'developer_portal';
694
- return status;
695
- }
696
+ if (devPortalTokenSet?.access_token) {
697
+ const status = TokenManager.getStatus(devPortalTokenSet);
698
+ status.authMode = 'developer_portal';
699
+ return status;
700
+ }
696
701
 
697
- return { authenticated: false, message: 'Not authenticated' };
698
- } catch (error) {
699
- return { authenticated: false, error: error.message, message: 'Error reading token status' };
702
+ return { authenticated: false, message: 'Not authenticated' };
703
+ } catch (error) {
704
+ return { authenticated: false, error: error.message, message: 'Error reading token status' };
705
+ }
700
706
  }
701
- }
702
707
 
703
- // -------------------------------------------------------------------------
704
- // Start Auth Handler
705
- // -------------------------------------------------------------------------
708
+ // -------------------------------------------------------------------------
709
+ // Start Auth Handler
710
+ // -------------------------------------------------------------------------
706
711
 
707
- async handleStartAuth(payload) {
708
- const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
712
+ async handleStartAuth(payload) {
713
+ const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
709
714
 
710
- if (!clientId || !clientSecret) {
711
- throw new Error('Client ID and Client Secret are required');
712
- }
713
- if (!callbackServerExternalAddress) {
714
- throw new Error('Callback Server Address is required');
715
- }
715
+ if (!clientId || !clientSecret) {
716
+ throw new Error('Client ID and Client Secret are required');
717
+ }
718
+ if (!callbackServerExternalAddress) {
719
+ throw new Error('Callback Server Address is required');
720
+ }
716
721
 
717
- const bindAddr = this.getCallbackBindAddress(payload);
718
- if (NetUtils.isLoopback(bindAddr) && !NetUtils.isLoopback(callbackServerExternalAddress)) {
722
+ const bindAddr = this.getCallbackBindAddress(payload);
723
+ if (NetUtils.isLoopback(bindAddr) && !NetUtils.isLoopback(callbackServerExternalAddress)) {
719
724
  // The browser is redirected to the external address, which can never reach a loopback-only server
720
- throw new Error(
721
- `The callback server bind address is ${bindAddr} (this machine only), but the callback address is ` +
725
+ throw new Error(
726
+ `The callback server bind address is ${bindAddr} (this machine only), but the callback address is ` +
722
727
  `${callbackServerExternalAddress}, so Daikin's redirect could never reach it. Set "Callback Server Bind Address" ` +
723
728
  'to 0.0.0.0 (or this host\'s LAN IP) in Settings > Network, then start authentication again.',
724
- );
725
- }
726
-
727
- const port = parseInt(callbackServerPort || String(DEFAULT_CALLBACK_PORT), 10);
728
- const redirectUri = `https://${callbackServerExternalAddress}:${port}`;
729
- const state = crypto.randomBytes(32).toString('hex');
730
-
731
- this.pendingAuth = { state, clientId, clientSecret, redirectUri, port, createdAt: Date.now() };
732
- this.authResult = null;
733
- this.invalidateDevicesCache();
734
-
735
- // Use static method from compiled src/api
736
- const authUrl = DaikinOAuth.buildAuthUrlStatic(clientId, redirectUri, state);
737
- console.log('[DaikinCloud] Redirect URI:', redirectUri);
738
-
739
- // Try to start callback server for automatic code capture
740
- let callbackServerRunning = false;
741
- let callbackServerError = null;
729
+ );
730
+ }
742
731
 
743
- try {
744
- await this.callbackServer.start({
745
- port,
746
- hostname: callbackServerExternalAddress,
747
- bindAddr,
748
- certDir: this.getCertDir(),
749
- requestHandler: this.handleHttpsCallback.bind(this),
750
- });
751
- callbackServerRunning = true;
752
- console.log('[DaikinCloud] Callback server started successfully');
753
- } catch (error) {
754
- callbackServerError = error.message;
755
- console.warn('[DaikinCloud] Failed to start callback server:', error.message);
756
- }
732
+ const port = parseInt(callbackServerPort || String(DEFAULT_CALLBACK_PORT), 10);
733
+ const redirectUri = `https://${callbackServerExternalAddress}:${port}`;
734
+ const state = crypto.randomBytes(32).toString('hex');
757
735
 
758
- return {
759
- authUrl,
760
- state,
761
- redirectUri,
762
- callbackServerRunning,
763
- callbackServerError,
764
- message: callbackServerRunning
765
- ? 'Callback server is running. Authentication will complete automatically.'
766
- : 'Could not start callback server. After authenticating, copy the full callback URL and paste it below.',
767
- };
768
- }
736
+ this.pendingAuth = { state, clientId, clientSecret, redirectUri, port, createdAt: Date.now() };
737
+ this.authResult = null;
738
+ this.invalidateDevicesCache();
769
739
 
770
- // -------------------------------------------------------------------------
771
- // HTTPS Callback Handler
772
- // -------------------------------------------------------------------------
740
+ // Use static method from compiled src/api
741
+ const authUrl = DaikinOAuth.buildAuthUrlStatic(clientId, redirectUri, state);
742
+ console.log('[DaikinCloud] Redirect URI:', redirectUri);
773
743
 
774
- handleHttpsCallback(req, res) {
775
- let url;
776
- try {
777
- url = new URL(req.url, 'https://localhost');
778
- } catch (e) {
779
- this.sendCallbackResponse(res, false, 'Invalid request', 400);
780
- return;
781
- }
744
+ // Try to start callback server for automatic code capture
745
+ let callbackServerRunning = false;
746
+ let callbackServerError = null;
782
747
 
783
- const code = url.searchParams.get('code');
784
- const state = url.searchParams.get('state');
785
- const error = url.searchParams.get('error');
786
- const errorDescription = url.searchParams.get('error_description');
748
+ try {
749
+ await this.callbackServer.start({
750
+ port,
751
+ hostname: callbackServerExternalAddress,
752
+ bindAddr,
753
+ certDir: this.getCertDir(),
754
+ requestHandler: this.handleHttpsCallback.bind(this),
755
+ });
756
+ callbackServerRunning = true;
757
+ console.log('[DaikinCloud] Callback server started successfully');
758
+ } catch (error) {
759
+ callbackServerError = error.message;
760
+ console.warn('[DaikinCloud] Failed to start callback server:', error.message);
761
+ }
787
762
 
788
- // Requests that do not carry the pending state are not ours: reject them
789
- // without touching authResult so they cannot disrupt a running login.
790
- if (!this.pendingAuth || !state || state !== this.pendingAuth.state) {
791
- this.sendCallbackResponse(res, false, 'Invalid or missing state parameter', 400);
792
- return;
763
+ return {
764
+ authUrl,
765
+ state,
766
+ redirectUri,
767
+ callbackServerRunning,
768
+ callbackServerError,
769
+ message: callbackServerRunning
770
+ ? 'Callback server is running. Authentication will complete automatically.'
771
+ : 'Could not start callback server. After authenticating, copy the full callback URL and paste it below.',
772
+ };
793
773
  }
794
774
 
795
- if (this.isPendingAuthExpired()) {
796
- this.pendingAuth = null;
797
- this.authResult = { success: false, error: 'Authorization request expired. Please try again.' };
798
- this.sendCallbackResponse(res, false, 'Authorization request expired. Please start again from Homebridge.', 400);
799
- this.callbackServer.stop().catch(() => {});
800
- return;
801
- }
775
+ // -------------------------------------------------------------------------
776
+ // HTTPS Callback Handler
777
+ // -------------------------------------------------------------------------
802
778
 
803
- if (error) {
804
- this.authResult = { success: false, error: errorDescription || error };
805
- this.sendCallbackResponse(res, false, errorDescription || error);
806
- return;
807
- }
779
+ handleHttpsCallback(req, res) {
780
+ let url;
781
+ try {
782
+ url = new URL(req.url, 'https://localhost');
783
+ } catch (e) {
784
+ this.sendCallbackResponse(res, false, 'Invalid request', 400);
785
+ return;
786
+ }
808
787
 
809
- if (!code) {
810
- this.authResult = { success: false, error: 'Missing code parameter' };
811
- this.sendCallbackResponse(res, false, 'Missing authorization code', 400);
812
- return;
813
- }
788
+ const code = url.searchParams.get('code');
789
+ const state = url.searchParams.get('state');
790
+ const error = url.searchParams.get('error');
791
+ const errorDescription = url.searchParams.get('error_description');
814
792
 
815
- const { clientId, clientSecret, redirectUri } = this.pendingAuth;
793
+ // Requests that do not carry the pending state are not ours: reject them
794
+ // without touching authResult so they cannot disrupt a running login.
795
+ if (!this.pendingAuth || !state || state !== this.pendingAuth.state) {
796
+ this.sendCallbackResponse(res, false, 'Invalid or missing state parameter', 400);
797
+ return;
798
+ }
816
799
 
817
- // Use static method from compiled src/api
818
- return DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri)
819
- .then((tokenSet) => {
820
- TokenManager.save(this.getTokenFilePath(), tokenSet);
821
- this.invalidateDevicesCache();
822
- this.authResult = {
823
- success: true,
824
- message: 'Authentication successful!',
825
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
826
- };
800
+ if (this.isPendingAuthExpired()) {
827
801
  this.pendingAuth = null;
828
- this.sendCallbackResponse(res, true, 'Authentication successful! You can close this window.');
802
+ this.authResult = { success: false, error: 'Authorization request expired. Please try again.' };
803
+ this.sendCallbackResponse(res, false, 'Authorization request expired. Please start again from Homebridge.', 400);
829
804
  this.callbackServer.stop().catch(() => {});
830
- })
831
- .catch((err) => {
832
- this.authResult = { success: false, error: err.message };
833
- this.sendCallbackResponse(res, false, `Token exchange failed: ${err.message}`);
834
- });
835
- }
805
+ return;
806
+ }
836
807
 
837
- sendCallbackResponse(res, success, message, statusCode = 200) {
838
- res.writeHead(statusCode, CALLBACK_RESPONSE_HEADERS);
839
- res.end(HtmlTemplates.callbackResponse(success, message));
840
- }
808
+ if (error) {
809
+ this.authResult = { success: false, error: errorDescription || error };
810
+ this.sendCallbackResponse(res, false, errorDescription || error);
811
+ return;
812
+ }
841
813
 
842
- // -------------------------------------------------------------------------
843
- // Manual Callback Handler
844
- // -------------------------------------------------------------------------
814
+ if (!code) {
815
+ this.authResult = { success: false, error: 'Missing code parameter' };
816
+ this.sendCallbackResponse(res, false, 'Missing authorization code', 400);
817
+ return;
818
+ }
845
819
 
846
- async handleCallback(payload) {
847
- let { code, state, callbackUrl } = payload;
820
+ const { clientId, clientSecret, redirectUri } = this.pendingAuth;
848
821
 
849
- if (callbackUrl) {
850
- try {
851
- const url = new URL(callbackUrl);
852
- code = url.searchParams.get('code');
853
- state = url.searchParams.get('state');
854
- } catch (e) {
855
- throw new Error('Invalid callback URL format', { cause: e });
856
- }
822
+ // Use static method from compiled src/api
823
+ return DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri)
824
+ .then((tokenSet) => {
825
+ TokenManager.save(this.getTokenFilePath(), tokenSet);
826
+ this.invalidateDevicesCache();
827
+ this.authResult = {
828
+ success: true,
829
+ message: 'Authentication successful!',
830
+ expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
831
+ };
832
+ this.pendingAuth = null;
833
+ this.sendCallbackResponse(res, true, 'Authentication successful! You can close this window.');
834
+ this.callbackServer.stop().catch(() => {});
835
+ })
836
+ .catch((err) => {
837
+ this.authResult = { success: false, error: err.message };
838
+ this.sendCallbackResponse(res, false, `Token exchange failed: ${err.message}`);
839
+ });
857
840
  }
858
841
 
859
- if (!code) {
860
- throw new Error('Authorization code is required');
861
- }
862
- if (!this.pendingAuth) {
863
- throw new Error('No pending authorization. Please start the auth flow again.');
864
- }
865
- if (!state || state !== this.pendingAuth.state) {
866
- throw new Error('Invalid or missing state parameter. Please paste the full callback URL or start again.');
867
- }
868
- if (this.isPendingAuthExpired()) {
869
- this.pendingAuth = null;
870
- throw new Error('Authorization request expired. Please try again.');
842
+ sendCallbackResponse(res, success, message, statusCode = 200) {
843
+ res.writeHead(statusCode, CALLBACK_RESPONSE_HEADERS);
844
+ res.end(HtmlTemplates.callbackResponse(success, message));
871
845
  }
872
846
 
873
- const { clientId, clientSecret, redirectUri } = this.pendingAuth;
847
+ // -------------------------------------------------------------------------
848
+ // Manual Callback Handler
849
+ // -------------------------------------------------------------------------
874
850
 
875
- try {
876
- // Use static method from compiled src/api
877
- const tokenSet = await DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri);
878
- TokenManager.save(this.getTokenFilePath(), tokenSet);
879
- this.invalidateDevicesCache();
880
- this.pendingAuth = null;
851
+ async handleCallback(payload) {
852
+ let { code, state, callbackUrl } = payload;
881
853
 
882
- await this.callbackServer.stop();
854
+ if (callbackUrl) {
855
+ try {
856
+ const url = new URL(callbackUrl);
857
+ code = url.searchParams.get('code');
858
+ state = url.searchParams.get('state');
859
+ } catch (e) {
860
+ throw new Error('Invalid callback URL format', { cause: e });
861
+ }
862
+ }
883
863
 
884
- return {
885
- success: true,
886
- message: 'Authentication successful! Restart Homebridge to apply.',
887
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
888
- };
889
- } catch (error) {
890
- throw new Error(`Token exchange failed: ${error.message}`, { cause: error });
891
- }
892
- }
864
+ if (!code) {
865
+ throw new Error('Authorization code is required');
866
+ }
867
+ if (!this.pendingAuth) {
868
+ throw new Error('No pending authorization. Please start the auth flow again.');
869
+ }
870
+ if (!state || state !== this.pendingAuth.state) {
871
+ throw new Error('Invalid or missing state parameter. Please paste the full callback URL or start again.');
872
+ }
873
+ if (this.isPendingAuthExpired()) {
874
+ this.pendingAuth = null;
875
+ throw new Error('Authorization request expired. Please try again.');
876
+ }
893
877
 
894
- // -------------------------------------------------------------------------
895
- // Poll Auth Result Handler
896
- // -------------------------------------------------------------------------
878
+ const { clientId, clientSecret, redirectUri } = this.pendingAuth;
879
+
880
+ try {
881
+ // Use static method from compiled src/api
882
+ const tokenSet = await DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri);
883
+ TokenManager.save(this.getTokenFilePath(), tokenSet);
884
+ this.invalidateDevicesCache();
885
+ this.pendingAuth = null;
897
886
 
898
- async handlePollAuthResult() {
899
- if (this.authResult) {
900
- const result = { ...this.authResult };
901
- if (result.success) {
902
- this.authResult = null;
903
887
  await this.callbackServer.stop();
888
+
889
+ return {
890
+ success: true,
891
+ message: 'Authentication successful! Restart Homebridge to apply.',
892
+ expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
893
+ };
894
+ } catch (error) {
895
+ throw new Error(`Token exchange failed: ${error.message}`, { cause: error });
904
896
  }
905
- return result;
906
897
  }
907
898
 
908
- const tokenSet = TokenManager.load(this.getTokenFilePath());
909
- if (tokenSet && tokenSet.access_token) {
910
- return {
911
- success: true,
912
- message: 'Authentication successful!',
913
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
914
- };
899
+ // -------------------------------------------------------------------------
900
+ // Poll Auth Result Handler
901
+ // -------------------------------------------------------------------------
902
+
903
+ async handlePollAuthResult() {
904
+ if (this.authResult) {
905
+ const result = { ...this.authResult };
906
+ if (result.success) {
907
+ this.authResult = null;
908
+ await this.callbackServer.stop();
909
+ }
910
+ return result;
911
+ }
912
+
913
+ const tokenSet = TokenManager.load(this.getTokenFilePath());
914
+ if (tokenSet && tokenSet.access_token) {
915
+ return {
916
+ success: true,
917
+ message: 'Authentication successful!',
918
+ expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
919
+ };
920
+ }
921
+ return { pending: true };
915
922
  }
916
- return { pending: true };
917
- }
918
923
 
919
- // -------------------------------------------------------------------------
920
- // Stop Server Handler
921
- // -------------------------------------------------------------------------
924
+ // -------------------------------------------------------------------------
925
+ // Stop Server Handler
926
+ // -------------------------------------------------------------------------
922
927
 
923
- async handleStopServer() {
924
- this.pendingAuth = null;
925
- this.authResult = null;
926
- await this.callbackServer.stop();
927
- return { success: true };
928
- }
928
+ async handleStopServer() {
929
+ this.pendingAuth = null;
930
+ this.authResult = null;
931
+ await this.callbackServer.stop();
932
+ return { success: true };
933
+ }
929
934
 
930
- // -------------------------------------------------------------------------
931
- // Mobile Auth Test Handler
932
- // -------------------------------------------------------------------------
935
+ // -------------------------------------------------------------------------
936
+ // Mobile Auth Test Handler
937
+ // -------------------------------------------------------------------------
933
938
 
934
- async handleMobileAuthTest(payload) {
935
- const { email, password } = payload;
939
+ async handleMobileAuthTest(payload) {
940
+ const { email, password } = payload;
936
941
 
937
- if (!email || !password) {
938
- return { success: false, message: 'Email and password are required' };
939
- }
942
+ if (!email || !password) {
943
+ return { success: false, message: 'Email and password are required' };
944
+ }
940
945
 
941
- const tokenFilePath = this.getMobileTokenFilePath();
942
- this.invalidateDevicesCache();
946
+ const tokenFilePath = this.getMobileTokenFilePath();
947
+ this.invalidateDevicesCache();
943
948
 
944
- try {
949
+ try {
945
950
  // Create a temporary mobile OAuth client
946
- const mobileOAuth = new DaikinMobileOAuth({
947
- email,
948
- password,
949
- tokenFilePath,
950
- });
951
+ const mobileOAuth = new DaikinMobileOAuth({
952
+ email,
953
+ password,
954
+ tokenFilePath,
955
+ });
951
956
 
952
- // Perform authentication
953
- console.log('[DaikinCloud] Testing mobile app authentication...');
954
- const tokenSet = await mobileOAuth.authenticate();
955
- console.log('[DaikinCloud] Mobile authentication successful');
957
+ // Perform authentication
958
+ console.log('[DaikinCloud] Testing mobile app authentication...');
959
+ const tokenSet = await mobileOAuth.authenticate();
960
+ console.log('[DaikinCloud] Mobile authentication successful');
956
961
 
957
- // Test API access and get device count
958
- let deviceCount = 0;
959
- let rateLimit = null;
962
+ // Test API access and get device count
963
+ let deviceCount = 0;
964
+ let rateLimit = null;
960
965
 
961
- try {
962
- const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
963
- deviceCount = Array.isArray(result.data) ? result.data.length : 0;
964
- rateLimit = result.rateLimit;
965
- } catch (apiError) {
966
- console.warn('[DaikinCloud] API test failed:', apiError.message);
967
- }
966
+ try {
967
+ const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
968
+ deviceCount = Array.isArray(result.data) ? result.data.length : 0;
969
+ rateLimit = result.rateLimit;
970
+ } catch (apiError) {
971
+ console.warn('[DaikinCloud] API test failed:', apiError.message);
972
+ }
968
973
 
969
- return {
970
- success: true,
971
- message: 'Authentication successful!',
972
- deviceCount,
973
- rateLimit,
974
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
975
- };
976
- } catch (error) {
977
- console.error('[DaikinCloud] Mobile auth test failed:', error.message);
978
- return {
979
- success: false,
980
- message: error.message || 'Authentication failed',
981
- };
974
+ return {
975
+ success: true,
976
+ message: 'Authentication successful!',
977
+ deviceCount,
978
+ rateLimit,
979
+ expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
980
+ };
981
+ } catch (error) {
982
+ console.error('[DaikinCloud] Mobile auth test failed:', error.message);
983
+ return {
984
+ success: false,
985
+ message: error.message || 'Authentication failed',
986
+ };
987
+ }
982
988
  }
983
- }
984
989
 
985
- // -------------------------------------------------------------------------
986
- // Revoke Auth Handler
987
- // -------------------------------------------------------------------------
990
+ // -------------------------------------------------------------------------
991
+ // Revoke Auth Handler
992
+ // -------------------------------------------------------------------------
988
993
 
989
- async handleRevokeAuth(payload) {
990
- this.invalidateDevicesCache();
994
+ async handleRevokeAuth(payload) {
995
+ this.invalidateDevicesCache();
991
996
 
992
- const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
993
- const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
997
+ const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
998
+ const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
994
999
 
995
- if (!devPortalTokenSet && !mobileTokenSet) {
996
- return { success: true, message: 'No tokens to revoke' };
997
- }
1000
+ if (!devPortalTokenSet && !mobileTokenSet) {
1001
+ return { success: true, message: 'No tokens to revoke' };
1002
+ }
998
1003
 
999
- const { clientId, clientSecret } = payload;
1004
+ const { clientId, clientSecret } = payload;
1000
1005
 
1001
- if (devPortalTokenSet?.refresh_token && clientId && clientSecret) {
1002
- try {
1006
+ if (devPortalTokenSet?.refresh_token && clientId && clientSecret) {
1007
+ try {
1003
1008
  // Use static method from compiled src/api
1004
- await DaikinOAuth.revokeTokenStatic(devPortalTokenSet.refresh_token, clientId, clientSecret);
1005
- } catch (error) {
1006
- console.warn('Failed to revoke token at server:', error.message);
1009
+ await DaikinOAuth.revokeTokenStatic(devPortalTokenSet.refresh_token, clientId, clientSecret);
1010
+ } catch (error) {
1011
+ console.warn('Failed to revoke token at server:', error.message);
1012
+ }
1007
1013
  }
1008
- }
1009
1014
 
1010
- // Delete both token files
1011
- TokenManager.delete(this.getTokenFilePath());
1012
- TokenManager.delete(this.getMobileTokenFilePath());
1013
- return { success: true, message: 'Authentication revoked. You will need to re-authenticate.' };
1014
- }
1015
+ // Delete both token files
1016
+ TokenManager.delete(this.getTokenFilePath());
1017
+ TokenManager.delete(this.getMobileTokenFilePath());
1018
+ return { success: true, message: 'Authentication revoked. You will need to re-authenticate.' };
1019
+ }
1015
1020
 
1016
- // -------------------------------------------------------------------------
1017
- // Test Connection Handler
1018
- // -------------------------------------------------------------------------
1021
+ // -------------------------------------------------------------------------
1022
+ // Test Connection Handler
1023
+ // -------------------------------------------------------------------------
1019
1024
 
1020
- async handleTestConnection() {
1021
- const tokenSet = this.getActiveTokenSet();
1022
- if (!tokenSet?.access_token) {
1023
- return { success: false, message: 'Not authenticated. Please authenticate first.' };
1024
- }
1025
+ async handleTestConnection() {
1026
+ const tokenSet = this.getActiveTokenSet();
1027
+ if (!tokenSet?.access_token) {
1028
+ return { success: false, message: 'Not authenticated. Please authenticate first.' };
1029
+ }
1025
1030
 
1026
- try {
1031
+ try {
1027
1032
  // Use static method from compiled src/api
1028
- const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
1029
- const devices = result.data;
1030
- return {
1031
- success: true,
1032
- message: `Connection successful! Found ${Array.isArray(devices) ? devices.length : 0} device(s).`,
1033
- deviceCount: Array.isArray(devices) ? devices.length : 0,
1034
- };
1035
- } catch (error) {
1036
- return { success: false, message: `Connection failed: ${error.message}`, error: error.message };
1033
+ const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
1034
+ const devices = result.data;
1035
+ return {
1036
+ success: true,
1037
+ message: `Connection successful! Found ${Array.isArray(devices) ? devices.length : 0} device(s).`,
1038
+ deviceCount: Array.isArray(devices) ? devices.length : 0,
1039
+ };
1040
+ } catch (error) {
1041
+ return { success: false, message: `Connection failed: ${error.message}`, error: error.message };
1042
+ }
1037
1043
  }
1038
- }
1039
1044
 
1040
- // -------------------------------------------------------------------------
1041
- // List Devices Handler
1042
- // -------------------------------------------------------------------------
1045
+ // -------------------------------------------------------------------------
1046
+ // List Devices Handler
1047
+ // -------------------------------------------------------------------------
1043
1048
 
1044
- async handleListDevices(payload) {
1049
+ async handleListDevices(payload) {
1045
1050
  // Token for the requested mode, or fall back to the active token
1046
- const { tokenSet, mode } = this.resolveToken(payload?.mode);
1051
+ const { tokenSet, mode } = this.resolveToken(payload?.mode);
1047
1052
 
1048
- if (!tokenSet?.access_token) {
1049
- return { success: false, devices: [], message: 'Not authenticated. Please authenticate first.' };
1050
- }
1053
+ if (!tokenSet?.access_token) {
1054
+ return { success: false, devices: [], message: 'Not authenticated. Please authenticate first.' };
1055
+ }
1051
1056
 
1052
- // Reuse a recent result to spare the daily API quota (explicit refresh bypasses it)
1053
- if (payload?.refresh) {
1054
- this.devicesCache.delete(mode);
1055
- }
1056
- const cached = this.devicesCache.get(mode);
1057
- if (cached && Date.now() - cached.fetchedAt < DEVICES_CACHE_TTL_MS) {
1058
- return { ...cached.result, cached: true, fetchedAt: new Date(cached.fetchedAt).toISOString() };
1059
- }
1057
+ // Reuse a recent result to spare the daily API quota (explicit refresh bypasses it)
1058
+ if (payload?.refresh) {
1059
+ this.devicesCache.delete(mode);
1060
+ }
1061
+ const cached = this.devicesCache.get(mode);
1062
+ if (cached && Date.now() - cached.fetchedAt < DEVICES_CACHE_TTL_MS) {
1063
+ return { ...cached.result, cached: true, fetchedAt: new Date(cached.fetchedAt).toISOString() };
1064
+ }
1060
1065
 
1061
- try {
1066
+ try {
1062
1067
  // Use static method from compiled src/api
1063
- const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
1064
- const gatewayDevices = result.data;
1065
- const devices = Array.isArray(gatewayDevices)
1066
- ? gatewayDevices.map(device => DeviceExtractor.extractAll(device))
1067
- : [];
1068
-
1069
- const response = { success: true, devices, message: `Found ${devices.length} device(s).` };
1070
- const fetchedAt = Date.now();
1071
- this.devicesCache.set(mode, { result: response, fetchedAt });
1072
- return { ...response, cached: false, fetchedAt: new Date(fetchedAt).toISOString() };
1073
- } catch (error) {
1074
- return { success: false, devices: [], message: `Failed to fetch devices: ${error.message}`, error: error.message };
1068
+ const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
1069
+ const gatewayDevices = result.data;
1070
+ const devices = Array.isArray(gatewayDevices)
1071
+ ? gatewayDevices.map(device => DeviceExtractor.extractAll(device))
1072
+ : [];
1073
+
1074
+ const response = { success: true, devices, message: `Found ${devices.length} device(s).` };
1075
+ const fetchedAt = Date.now();
1076
+ this.devicesCache.set(mode, { result: response, fetchedAt });
1077
+ return { ...response, cached: false, fetchedAt: new Date(fetchedAt).toISOString() };
1078
+ } catch (error) {
1079
+ return { success: false, devices: [], message: `Failed to fetch devices: ${error.message}`, error: error.message };
1080
+ }
1075
1081
  }
1076
- }
1077
1082
 
1078
- // -------------------------------------------------------------------------
1079
- // Get Rate Limit Handler
1080
- // -------------------------------------------------------------------------
1083
+ // -------------------------------------------------------------------------
1084
+ // Get Rate Limit Handler
1085
+ // -------------------------------------------------------------------------
1081
1086
 
1082
- /**
1087
+ /**
1083
1088
  * Report the rate limit status the running plugin records after each API
1084
1089
  * response, instead of spending an API call just to read the headers.
1085
1090
  */
1086
- async handleGetRateLimit() {
1087
- let status;
1088
- try {
1089
- status = JSON.parse(fs.readFileSync(this.getRateLimitFilePath(), 'utf8'));
1090
- } catch (error) {
1091
- if (error.code === 'ENOENT') {
1092
- return {
1093
- success: false,
1094
- noData: true,
1095
- message: 'No data yet — the plugin records this after its next API call.',
1096
- };
1091
+ async handleGetRateLimit() {
1092
+ let status;
1093
+ try {
1094
+ status = JSON.parse(fs.readFileSync(this.getRateLimitFilePath(), 'utf8'));
1095
+ } catch (error) {
1096
+ if (error.code === 'ENOENT') {
1097
+ return {
1098
+ success: false,
1099
+ noData: true,
1100
+ message: 'No data yet — the plugin records this after its next API call.',
1101
+ };
1102
+ }
1103
+ return { success: false, message: `Could not read rate limit status: ${error.message}` };
1097
1104
  }
1098
- return { success: false, message: `Could not read rate limit status: ${error.message}` };
1099
- }
1100
1105
 
1101
- if (!status || typeof status !== 'object') {
1102
- return { success: false, message: 'Could not read rate limit status: invalid file contents' };
1103
- }
1106
+ if (!status || typeof status !== 'object') {
1107
+ return { success: false, message: 'Could not read rate limit status: invalid file contents' };
1108
+ }
1104
1109
 
1105
- const { limitMinute, remainingMinute, limitDay, remainingDay, mode, updatedAt } = status;
1106
- const updatedAtMs = typeof updatedAt === 'number' ? updatedAt : Date.parse(updatedAt);
1107
- const hasTime = Number.isFinite(updatedAtMs);
1110
+ const { limitMinute, remainingMinute, limitDay, remainingDay, mode, updatedAt } = status;
1111
+ const updatedAtMs = typeof updatedAt === 'number' ? updatedAt : Date.parse(updatedAt);
1112
+ const hasTime = Number.isFinite(updatedAtMs);
1108
1113
 
1109
- return {
1110
- success: true,
1111
- rateLimit: { limitMinute, remainingMinute, limitDay, remainingDay },
1112
- mode: mode || null,
1113
- updatedAt: hasTime ? new Date(updatedAtMs).toISOString() : null,
1114
- ageMs: hasTime ? Math.max(0, Date.now() - updatedAtMs) : null,
1115
- };
1116
- }
1114
+ return {
1115
+ success: true,
1116
+ rateLimit: { limitMinute, remainingMinute, limitDay, remainingDay },
1117
+ mode: mode || null,
1118
+ updatedAt: hasTime ? new Date(updatedAtMs).toISOString() : null,
1119
+ ageMs: hasTime ? Math.max(0, Date.now() - updatedAtMs) : null,
1120
+ };
1121
+ }
1117
1122
 
1118
- // -------------------------------------------------------------------------
1119
- // Validate Config Handler
1120
- // -------------------------------------------------------------------------
1123
+ // -------------------------------------------------------------------------
1124
+ // Validate Config Handler
1125
+ // -------------------------------------------------------------------------
1121
1126
 
1122
- /**
1127
+ /**
1123
1128
  * Validate the Developer Portal wizard fields with the plugin's own rules
1124
1129
  * (ConfigManager), so the UI and the plugin can never disagree.
1125
1130
  * Response: { valid, errors, warnings }.
1126
1131
  */
1127
- async handleValidateConfig(payload) {
1128
- const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload || {};
1129
- return validateConfig({
1130
- platform: 'DaikinCloud',
1131
- authMode: 'developer_portal',
1132
- clientId,
1133
- clientSecret,
1134
- callbackServerExternalAddress,
1135
- callbackServerPort,
1136
- });
1137
- }
1132
+ async handleValidateConfig(payload) {
1133
+ const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload || {};
1134
+ return validateConfig({
1135
+ platform: 'DaikinCloud',
1136
+ authMode: 'developer_portal',
1137
+ clientId,
1138
+ clientSecret,
1139
+ callbackServerExternalAddress,
1140
+ callbackServerPort,
1141
+ });
1142
+ }
1143
+ };
1138
1144
  }
1139
1145
 
1140
1146
  // =============================================================================
1141
1147
  // Initialize Server
1142
1148
  // =============================================================================
1143
1149
 
1150
+ /**
1151
+ * Load @homebridge/plugin-ui-utils synchronously when the runtime supports
1152
+ * require() of ES modules, otherwise return null so the caller uses import().
1153
+ */
1154
+ function loadUiUtilsSync() {
1155
+ try {
1156
+ return require('@homebridge/plugin-ui-utils');
1157
+ } catch (error) {
1158
+ if (error.code === 'ERR_REQUIRE_ESM') {
1159
+ return null;
1160
+ }
1161
+ throw error;
1162
+ }
1163
+ }
1164
+
1165
+ async function loadUiUtils() {
1166
+ return loadUiUtilsSync() || import('@homebridge/plugin-ui-utils');
1167
+ }
1168
+
1169
+ const uiUtils = loadUiUtilsSync();
1170
+
1144
1171
  module.exports = {
1145
- DaikinCloudUiServer,
1172
+ // undefined on runtimes without require(esm); use createDaikinCloudUiServer
1173
+ DaikinCloudUiServer: uiUtils ? createDaikinCloudUiServer(uiUtils.HomebridgePluginUiServer) : undefined,
1174
+ createDaikinCloudUiServer,
1175
+ loadUiUtils,
1146
1176
  CallbackServer,
1147
1177
  SSLUtils,
1148
1178
  NetUtils,
@@ -1156,5 +1186,13 @@ module.exports = {
1156
1186
  // Homebridge UI forks this file as a child process; only start the server then,
1157
1187
  // so tests can require it without side effects.
1158
1188
  if (require.main === module) {
1159
- (() => new DaikinCloudUiServer())();
1189
+ loadUiUtils()
1190
+ .then(({ HomebridgePluginUiServer }) => {
1191
+ const DaikinCloudUiServer = createDaikinCloudUiServer(HomebridgePluginUiServer);
1192
+ return new DaikinCloudUiServer();
1193
+ })
1194
+ .catch((error) => {
1195
+ console.error('Failed to start the Daikin Cloud UI server:', error);
1196
+ process.exit(1);
1197
+ });
1160
1198
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "displayName": "Daikin Cloud",
3
3
  "platformname": "daikincloud",
4
4
  "name": "@mp-consulting/homebridge-daikin-cloud",
5
- "version": "1.6.0",
5
+ "version": "1.6.1",
6
6
  "description": "Integrate with the Daikin Cloud to control your Daikin air conditioning via the cloud",
7
7
  "license": "(Apache-2.0 AND MIT)",
8
8
  "repository": {
@@ -25,6 +25,7 @@
25
25
  "test:coverage": "vitest run --coverage",
26
26
  "lint": "eslint . --max-warnings=0",
27
27
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
28
+ "smoke:ui": "node scripts/smoke-ui-server.js",
28
29
  "schema:check": "tsx scripts/check-schema-drift.ts",
29
30
  "watch": "npm run build && npm link && nodemon",
30
31
  "copy:ui-kit": "mkdir -p homebridge-ui/public/lib && cp node_modules/@mp-consulting/homebridge-ui-kit/dist/kit.css homebridge-ui/public/lib/ && cp node_modules/@mp-consulting/homebridge-ui-kit/dist/kit.js homebridge-ui/public/lib/",