homebridge-myplace 2.3.3 → 2.3.5

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/CHANGELOG.md CHANGED
@@ -1,3 +1,4 @@
1
1
  ### Homebridge-myplace - An independent plugin for Homebridge bringing Advantage Air MyPlace system, its smaller siblings (E-zone, MyAir, MyAir4, etc) and its cousins (e.g. Fujitsu AnywAir) to Homekit
2
- ##### v2.3.3 (18-11-2025)
3
- ##### (1) Bug fix - to fix an error of an undefined variable.
2
+
3
+ ##### v2.3.5 (14-01-2026)
4
+ ##### (1) Determine device accessibility by validating the port's availability rather than initiating a direct access request. This approach prevents multiple access attempts occuring in rapid succession during the stup porcess.
package/Cmd5Platform.js CHANGED
@@ -123,7 +123,7 @@ class Cmd5Platform
123
123
  this.processNewCharacteristicDefinitions( );
124
124
 
125
125
  // scan the platform accessories (devices) to identify which ones to be restored from cache
126
- this.log.info( chalk.yellow( "***Scanning the config and the cache for accessories to be removed or restored from cache..." ) );
126
+ this.log.info( chalk.yellow( "*** Scanning the config and the cache for accessories to be removed or restored from cache..." ) );
127
127
  this.scanToBeRemovedOrRestored( this.log );
128
128
 
129
129
  // Any accessory NOT to be restored should be removed, find them
@@ -1,4 +1,12 @@
1
1
  ### Homebridge-myplace - An independent plugin for Homebridge bringing Advantage Air MyPlace system, its smaller siblings (E-zone, MyAir, MyAir4, etc) and its cousins (e.g. Fujitsu AnywAir) to Homekit
2
+
3
+ ##### v2.3.5 (14-01-2026)
4
+ ##### (1) Determine device accessibility by validating the port's availability rather than initiating a direct access request. This approach prevents multiple access attempts occuring in rapid succession during the stup porces>
5
+
6
+ ##### v2.3.4 (05-12-2025)
7
+ ##### (1) Ensure that all valid devices defined in the configuration are processed, even when one or more configured devices are invalid or inaccessible.
8
+ ##### (2) Improved the clarity and consistency of log messages.
9
+
2
10
  ##### v2.3.3 (18-11-2025)
3
11
  ##### (1) Bug fix - to fix an error of an undefined variable.
4
12
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-myplace",
3
3
  "description": "Exec Plugin bringing Advanatge Air MyPlace system to Homekit",
4
- "version": "2.3.3",
4
+ "version": "2.3.5",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Ung Sing"
@@ -1,7 +1,7 @@
1
1
  // This async function is to generate a complete configuration file needed for the myPlace plugin
2
2
  // It can handle up to 3 independent myPlace (BB) systems
3
3
 
4
- async function createMyPlaceConfig(config, pluginPath) {
4
+ async function createMyPlaceConfig(config, IPs, pluginPath, log) {
5
5
  const path = require("path");
6
6
 
7
7
  const MYPLACE_SH_PATH = path.join(pluginPath, "MyPlace.sh");
@@ -415,18 +415,6 @@ async function createMyPlaceConfig(config, pluginPath) {
415
415
  };
416
416
  }
417
417
 
418
- // Helper regex to validate IP and IP:port
419
- const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
420
- const ipPortRegex = /^(\d{1,3}\.){3}\d{1,3}:\d+$/;
421
-
422
- // Validate and normalize IPs
423
- function normalizeIP( ip, port = 2025 ) {
424
- if (!ip || ip === "undefined" ) return "";
425
- if (ipRegex.test(ip)) return `${ip}:${port}`;
426
- if (ipPortRegex.test(ip)) return ip;
427
- throw new Error(`ERROR: Device ${n + 1} - the specified IP address ${ip} is in wrong format`);
428
- }
429
-
430
418
  // Determine number of tablets/devices
431
419
  let noOfTablets = config.devices.length;
432
420
 
@@ -440,31 +428,40 @@ async function createMyPlaceConfig(config, pluginPath) {
440
428
  } else {
441
429
  AAname = config.devices[n]?.name || `Aircon${n + 1}`;
442
430
  }
443
- const AAIP = config.devices[n]?.ipAddress;
444
- const AAport = config.devices[n]?.port || 2025;
431
+
432
+ const ip = IPs[n];
433
+ if (ip === "undefined") continue;
434
+
445
435
  const extraTimers = config.devices[n]?.extraTimers || false;
446
436
  const debug = config.devices[n]?.debug || false;
447
437
  const queue=["AAA", "AAB", "AAC"][n]
448
438
 
449
- const ip = normalizeIP(AAIP, AAport);
450
439
  const IPA = `\${AAIP${n + 1}}`
451
440
 
452
- // if (!ip || ip === "undefined" || !ipRegex.test(ip)) continue;
453
-
454
441
  // Fetch system data
455
442
  let myAirData;
456
443
  try {
457
- const response = await fetch(`http://${ip}/getSystemData`, {timeout: 45000});
444
+ const response = await fetch(`http://${ip}/getSystemData`);
458
445
  if (!response.ok) throw new Error(`HTTP error ${response.status}`);
459
446
  myAirData = await response.json();
460
447
  } catch {
461
- throw new Error(`ERROR: Device ${n + 1} is inaccessible - not power ON or wrong IP ${AAIP} or wrong port ${AAport}`);
448
+ if (n === noOfTablets - 1) {
449
+ throw new Error(`Device ${n + 1} with IP ${ip} is inaccessible (power OFF or wrong IP or wrong port).`);
450
+ } else {
451
+ log.warn(`⚠️ Device ${n + 1} with IP ${ip} is inaccessible (power OFF or wrong IP or wrong port).`);
452
+ continue;
453
+ }
462
454
  }
463
455
 
464
456
  // Extract system info (use safe chaining or checks as needed)
465
457
  const sysName = (myAirData.system?.name ?? "").replace(/ /g, "_").replace(/['"]/g, "");
466
458
  if (!sysName) {
467
- throw new Error("ERROR: failed to get system info from your AdvantageAir system!");
459
+ if (n === noOfTablets - 1) {
460
+ throw new Error("Failed to get system info from your device ${n + 1}!");
461
+ } else {
462
+ log.error("⚠️ Failed to get system info from your device ${n + 1}!");
463
+ continue;
464
+ }
468
465
  }
469
466
  const sysType = (myAirData.system?.sysType ?? "").replace(/ /g, "_").replace(/"/g, "");
470
467
  const tspModel = (myAirData.system?.tspModel ?? "").replace(/ /g, "_").replace(/"/g, "");
@@ -47,7 +47,7 @@ async function devicesAutoDiscovery(config, log, portsToTry) {
47
47
  }
48
48
  }
49
49
 
50
- return { foundDevices, portsToTry };
50
+ return foundDevices;
51
51
  }
52
52
 
53
53
  module.exports = { devicesAutoDiscovery };
@@ -0,0 +1,63 @@
1
+ const net = require("net");
2
+
3
+ // Simple port check (returns true/false)
4
+ function isPortReachable( ip ) {
5
+ return new Promise((resolve) => {
6
+ const socket = new net.Socket();
7
+
8
+ socket.setTimeout(1000);
9
+
10
+ socket.on("connect", () => {
11
+ socket.destroy();
12
+ resolve(true);
13
+ });
14
+
15
+ socket.on("timeout", () => {
16
+ socket.destroy();
17
+ resolve(false);
18
+ });
19
+
20
+ socket.on("error", () => {
21
+ resolve(false);
22
+ });
23
+
24
+ const [ipStr, portStr] = ip.split(":");
25
+ const port = Number(portStr);
26
+
27
+ socket.connect(port, ipStr);
28
+ });
29
+ }
30
+
31
+ // If port is not reachable, retry up to 5 times...
32
+ async function isIpAccessible( ip, i, noOfDevices, log ) {
33
+
34
+ for (let attempt = 0; attempt < 6; attempt++) {
35
+
36
+ try {
37
+ const reachable = await isPortReachable( ip );
38
+
39
+ if (reachable) {
40
+ return true; // success
41
+ } else {
42
+ throw new Error("Port unreachable");
43
+ }
44
+
45
+ } catch (err) {
46
+
47
+ if (attempt === 5) {
48
+ log.warn(`⚠️ All 5 retry attempts on Device ${i + 1} failed!`);
49
+ return false;
50
+ }
51
+
52
+ log.warn(
53
+ `⚠️ Device ${i + 1}/${noOfDevices} with IP ${ip} is inaccessible. ` +
54
+ `Retrying (${attempt + 1}/5) in 5s...`
55
+ );
56
+
57
+ // wait 5 seconds before retry
58
+ await new Promise(resolve => setTimeout(resolve, 5000));
59
+ }
60
+ }
61
+ }
62
+
63
+ module.exports = { isIpAccessible };
@@ -1,4 +1,5 @@
1
1
  // Update myplaceConfig
2
+ const { isIpAccessible } = require("./isIpAccessible");
2
3
  const { devicesAutoDiscovery } = require("./devicesAutoDiscovery");
3
4
  const { createMyPlaceConfig } = require("./createMyPlaceConfig");
4
5
  const { readConfig } = require("./readConfig");
@@ -9,6 +10,8 @@ function delay(ms) {
9
10
  return new Promise(resolve => setTimeout(resolve, ms));
10
11
  }
11
12
 
13
+ const isValidIp = (value) => /^(?:\d{1,3}\.){3}\d{1,3}$/.test(value);
14
+
12
15
  async function updateConfig(config, log, storagePath, pluginPath) {
13
16
  // Enforce maxAccessories: default 149 (Homebridge hard limit)
14
17
  const maxAccessories = (typeof config.maxAccessories === "number"
@@ -35,145 +38,113 @@ async function updateConfig(config, log, storagePath, pluginPath) {
35
38
  });
36
39
  }
37
40
 
41
+ let IPs = [];
38
42
  if (!Array.isArray(config.devices) || devicesMissingIPs) {
39
- log.warn(chalk.yellow("⚠️ No devices found in the original config — triggering auto-discovery..."));
43
+ log.warn(`⚠️ No devices found in the original config!`);
44
+ log.warn(`🔍 *** Triggering device auto-discovery...`);
40
45
  doDevicesAutoDiscovery = true;
46
+ } else {
47
+ // check if the device(s) in config is accessbile or not with retry up to 5 times, if not, set it to "undefined".
48
+ log.warn(`🕵️ *** Validating device IP address(es)...`);
49
+ const noOfDevices = config.devices.length;
50
+ for ( let i = 0; i < noOfDevices; i++ ) {
51
+ const ip = config.devices[i].ipAddress;
52
+ const port = config.devices[i].port || 2025;
53
+ if (ip) {
54
+ IPs.push(`${ip}:${port}`);
55
+ if ( !isValidIp(ip) ) {
56
+ log.warn(`⚠️ Device ${i + 1}/${noOfDevices} with IP ${ip}:${port} has its IP in wrong format!`);
57
+ log.warn(`⚠️ Device ${i + 1}/${noOfDevices} will NOT be processed!`);
58
+ IPs[i] = "undefined";
59
+ } else {
60
+ const isIpAccessibleTest = await isIpAccessible( `${ip}:${port}`, i, noOfDevices, log );
61
+ if ( !isIpAccessibleTest ) {
62
+ log.warn(`⚠️ Device ${i + 1}/${noOfDevices} with IP ${ip}:${port} is inaccessible! May be power OFF, wrong IP or wrong port.`);
63
+ log.warn(`⚠️ Device ${i + 1}/${noOfDevices} will NOT be processed!`);
64
+ IPs[i] = "undefined";
65
+ } else {
66
+ log.info(`✅ Device ${i + 1}/${noOfDevices} is reachable!`);
67
+ }
68
+ }
69
+ } else {
70
+ IPs.push("undefined");
71
+ }
72
+ }
73
+ // check that not all IPs are "undefined", if so, do devucesAutoDiscovery...
74
+ if (IPs.every((el) => el === "undefined")) {
75
+ // final attempt to auto discover a Bond device
76
+ log.warn(`⚠️ No specified device is accessible on the LAN network!`);
77
+ log.warn(`🔍 *** Triggering device auto-discovery...`);
78
+ doDevicesAutoDiscovery = true;
79
+ }
41
80
  }
42
81
 
43
- while (portsToTry.length > 0) {
44
- // Auto-discovery stage
45
- if (doDevicesAutoDiscovery) {
46
- log.info("🔍 Ports to scan for devices:", portsToTry);
47
- const { foundDevices, portsToTry: remainingPorts } =
48
- await devicesAutoDiscovery(config, log, portsToTry);
82
+ // Auto-discovery stage
83
+ if ( doDevicesAutoDiscovery ) {
84
+ log.info("🔍 Ports to scan for devices:", portsToTry);
85
+ const foundDevices = await devicesAutoDiscovery(config, log, portsToTry);
49
86
 
50
- portsToTry = remainingPorts;
51
- devicesAutoDiscoveryCounter++;
87
+ const noOfDevices = foundDevices.length;
88
+ if (noOfDevices > 0) {
89
+ config.devices = foundDevices;
90
+ log.info("Devices config:\n" + JSON.stringify(config.devices, null, 2));
52
91
 
53
- if (foundDevices.length > 0) {
54
- config.devices = foundDevices;
55
- log.info("Devices config:\n" + JSON.stringify(config.devices, null, 2));
92
+ // Store found devices IPs
93
+ for ( let i = 0; i < noOfDevices; i++ ) {
94
+ IPs[i] = foundDevices[i].ipAddress + ':' + foundDevices[i].port;
95
+ }
96
+ } else {
97
+ log.warn("⚠️ No devices found on any ports.");
98
+ // check if an existing config.json is present in this.storagePath/.myplace, if so use it
99
+ existingConfig = readConfig( storagePath, log );
100
+ if (existingConfig) {
101
+ log.warn("⚠️ Proceeding with existing config — all cached accessories will be restored.");
102
+ return existingConfig;
56
103
  } else {
57
- log.warn("⚠️ No devices found on any ports.");
58
- // check if an existing config.json is present in this.storagePath/.myplace, if so use it
59
- existingConfig = readConfig( storagePath, log );
60
- if (existingConfig) {
61
- log.warn("⚠️ Proceeding with existing config — all cached accessories will be restored.");
62
- return existingConfig;
63
- } else {
64
- log.warn("⚠️ Proceeding with original config — no accessories will be created and cached accessories will be removed!");
65
- return config;
66
- }
104
+ log.warn("⚠️ Proceeding with original config no accessories will be created and cached accessories will be removed!");
105
+ return config;
67
106
  }
68
107
  }
108
+ }
69
109
 
70
- // Run ConfigCreator
71
- log.info(chalk.yellow(
72
- devicesAutoDiscoveryCounter > 1
73
- ? "Running createMyPlaceConfig again..."
74
- : "Running createMyPlaceConfig..."
75
- ));
76
-
77
- try {
78
- const myplaceConfig = await createMyPlaceConfig(config, pluginPath);
79
- log.info(chalk.green("✅ DONE! createMyPlaceConfig completed successfully!"));
80
- log.debug("Updated MyPlace config:\n" + JSON.stringify(myplaceConfig, null, 2));
81
-
82
- // Enforce accessories limit
83
- if (Array.isArray(myplaceConfig.accessories) &&
84
- myplaceConfig.accessories.length > maxAccessories) {
85
- log.warn(`⚠️ Configured accessories exceed limit of ${maxAccessories}. ` +
86
- `Only the first ${maxAccessories} will be bridged, ` +
87
- `${myplaceConfig.accessories.length - maxAccessories} ignored.`);
88
- log.info("Note: Homebridge only allows a maximum of 149 bridged accessories per bridge.");
89
-
90
- myplaceConfig.accessories = myplaceConfig.accessories.slice(0, maxAccessories);
91
- }
110
+ // Run CreateMyPlaceConfig
111
+ log.warn(`*** Running createMyPlaceConfig...`);
112
+ try {
113
+ const noOfDevices = IPs.length;
114
+ const noOfDevicesProcessed = IPs.filter(ip => ip !== "undefined").length;
92
115
 
93
- return myplaceConfig;
94
- } catch (err) {
95
- const autoDiscoveryErrors = ["wrong format", "inaccessible"];
96
- const isInaccessibleError = err.message.toLowerCase().includes("inaccessible");
97
-
98
- doDevicesAutoDiscovery = autoDiscoveryErrors.some(e =>
99
- err.message.toLowerCase().includes(e)
100
- );
101
-
102
- if (doDevicesAutoDiscovery && portsToTry.length > 0) {
103
- log.warn(`⚠️ ${err.message}`);
104
-
105
- // Special handling for "inaccessible" errors - wait 5 seconds and retry up to 5 times
106
- if (isInaccessibleError) {
107
- const maxRetries = 5;
108
- let retryCount = 0;
109
- let retrySuccess = false;
110
- let lastRetryError = err;
111
-
112
- let deviceNumber = 1;
113
- let ipAddress;
114
- const match = err.message.match(/Device (\d+).*?IP ([\d.]+)/);
115
- if (match) {
116
- deviceNumber = parseInt(match[1], 10);
117
- ipAddress = match[2];
118
- }
116
+ const myplaceConfig = await createMyPlaceConfig(config, IPs, pluginPath, log);
117
+ if (doDevicesAutoDiscovery) {
118
+ log.info(`✅ DONE! createMyPlaceConfig completed successfully for ${noOfDevicesProcessed}/${noOfDevices} "auto-discovered" device(s)!`);
119
+ } else {
120
+ log.info(`✅ DONE! createMyPlaceConfig completed successfully for ${noOfDevicesProcessed}/${noOfDevices} device(s)!`);
121
+ }
119
122
 
120
- while (retryCount < maxRetries && !retrySuccess) {
121
- retryCount++;
122
- log.info(chalk.yellow(`⏳ Device ${deviceNumber} with IP ${ipAddress} is inaccessible, waiting 5 seconds before retry (${retryCount}/${maxRetries})...`));
123
- await delay(5000);
124
-
125
- // Retry createMyPlaceConfig immediately without auto-discovery
126
- try {
127
- log.info(chalk.yellow(`🔄 Retrying createMyPlaceConfig (attempt ${retryCount}/${maxRetries})...`));
128
- const myplaceConfig = await createMyPlaceConfig(config, pluginPath);
129
- log.info(chalk.green(`✅ SUCCESS! createMyPlaceConfig completed after ${retryCount} retry attempt(s)!`));
130
-
131
- // Enforce accessories limit on retry success
132
- if (Array.isArray(myplaceConfig.accessories) &&
133
- myplaceConfig.accessories.length > maxAccessories) {
134
- log.warn(`⚠️ Configured accessories exceed limit of ${maxAccessories}. ` +
135
- `Only the first ${maxAccessories} will be bridged, ` +
136
- `${myplaceConfig.accessories.length - maxAccessories} ignored.`);
137
- myplaceConfig.accessories = myplaceConfig.accessories.slice(0, maxAccessories);
138
- }
139
-
140
- return myplaceConfig;
141
- } catch (retryErr) {
142
- lastRetryError = retryErr;
143
- log.warn(`⚠️ Retry attempt ${retryCount}/${maxRetries} failed: ${retryErr.message}`);
144
-
145
- // Check if it's still an "inaccessible" error for the next retry
146
- if (!retryErr.message.toLowerCase().includes("inaccessible")) {
147
- log.warn("⚠️ Error type changed, stopping retries and proceeding to auto-discovery...");
148
- break;
149
- }
150
- }
151
- }
123
+ log.debug("Updated MyPlace config:\n" + JSON.stringify(myplaceConfig));
152
124
 
153
- // If we exhausted all retries and still failed, log the final error
154
- if (!retrySuccess) {
155
- log.error(`❌ All ${maxRetries} retry attempts failed. Last error: ${lastRetryError.message}`);
156
- }
157
- }
125
+ // Enforce accessories limit
126
+ if (Array.isArray(myplaceConfig.accessories) &&
127
+ myplaceConfig.accessories.length > maxAccessories) {
128
+ log.warn(`⚠️ Configured accessories exceed limit of ${maxAccessories}. ` +
129
+ `Only the first ${maxAccessories} will be bridged, ` +
130
+ `${myplaceConfig.accessories.length - maxAccessories} ignored.`);
131
+ log.info("Note: Homebridge only allows a maximum of 149 bridged accessories per bridge.");
158
132
 
159
- if (devicesAutoDiscoveryCounter === 0) {
160
- log.info(chalk.yellow("🔍 Starting devices auto-discovery..."));
161
- } else {
162
- log.info(chalk.yellow(`🔄 Retrying devices auto-discovery (attempt #${devicesAutoDiscoveryCounter + 1})...`));
163
- }
164
- } else {
165
- log.error(`❌ ${err.message}`);
166
- log.warn("⚠️ No devices found in your network!");
167
- // check if an existing config.json is present in this.storagePath/.myplace, if so use it
168
- existingConfig = readConfig( storagePath, log );
169
- if (existingConfig) {
170
- log.warn("⚠️ Proceeding with existing config — all cached accessories will be restored.");
171
- return existingConfig;
172
- }
173
- log.warn("⚠️ Proceeding with original config — no accessories will be created and cached accessories will be removed!");
174
- return config;
175
- }
133
+ myplaceConfig.accessories = myplaceConfig.accessories.slice(0, maxAccessories);
134
+ }
135
+
136
+ return myplaceConfig;
137
+ } catch (err) {
138
+ log.warn(`⚠️ ${err.message}`);
139
+ log.warn(`⚠️ Config not updated!`);
140
+ // check if an existing config.json is present in this.storagePath/.myplace, if so use it
141
+ existingConfig = readConfig( storagePath, log );
142
+ if (existingConfig) {
143
+ log.warn(`⚠️ Proceeding with existing config — all cached accessories will be restored.`);
144
+ return existingConfig;
176
145
  }
146
+ log.warn(`⚠️ Proceeding with original config — no accessories will be created and cached accessories will be removed!`);
147
+ return config;
177
148
  }
178
149
 
179
150
  return config; // fallback