node-red-contrib-homebridge-automation 0.1.12-beta.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/Build and Publish.yml +81 -75
- package/README.md +7 -4
- package/eslint.config.mjs +34 -0
- package/package.json +35 -26
- package/src/HAP-NodeRed.html +71 -71
- package/src/HAP-NodeRed.js +32 -1082
- package/src/HapDeviceRoutes.js +59 -0
- package/src/hbBaseNode.js +94 -0
- package/src/hbConfigNode.js +239 -0
- package/src/hbConfigNode.test.js +2179 -0
- package/src/hbControlNode.js +77 -0
- package/src/hbEventNode.js +23 -0
- package/src/hbResumeNode.js +63 -0
- package/src/hbStatusNode.js +37 -0
- package/test/node-red/.config.nodes.json +453 -0
- package/test/node-red/.config.nodes.json.backup +453 -0
- package/test/node-red/.config.runtime.json +4 -0
- package/test/node-red/.config.runtime.json.backup +3 -0
- package/test/node-red/.config.users.json +23 -0
- package/test/node-red/.config.users.json.backup +20 -0
- package/test/node-red/.flows.json.backup +2452 -0
- package/test/node-red/flows.json +2453 -0
- package/test/node-red/package.json +6 -0
- package/test/node-red/settings.js +593 -0
- package/test/node-red/test/node-red/.config.nodes.json +430 -0
- package/test/node-red/test/node-red/.config.runtime.json +4 -0
- package/test/node-red/test/node-red/.config.runtime.json.backup +3 -0
- package/test/node-red/test/node-red/.config.users.json +20 -0
- package/test/node-red/test/node-red/.config.users.json.backup +17 -0
- package/test/node-red/test/node-red/package.json +6 -0
- package/test/node-red/test/node-red/settings.js +593 -0
- package/.eslintrc.js +0 -24
- package/.github/npm-version-script.js +0 -93
- package/.nycrc.json +0 -11
- package/src/lib/Accessory.js +0 -126
- package/src/lib/Characteristic.js +0 -30
- package/src/lib/HbAccessories.js +0 -167
- package/src/lib/Homebridge.js +0 -71
- package/src/lib/Homebridges.js +0 -68
- package/src/lib/Service.js +0 -307
- package/src/lib/register.js +0 -156
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const debug = require('debug')('hapNodeRed:HapDeviceRoutes');
|
|
2
|
+
|
|
3
|
+
class HapDeviceRoutes {
|
|
4
|
+
constructor(RED) {
|
|
5
|
+
this.RED = RED;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// POST /hap-device/refresh/:id
|
|
9
|
+
refreshDevice(req, res) {
|
|
10
|
+
const conf = this.RED.nodes.getNode(req.params.id);
|
|
11
|
+
if (conf) {
|
|
12
|
+
res.status(200).send();
|
|
13
|
+
} else {
|
|
14
|
+
debug("Can't refresh until deployed");
|
|
15
|
+
res.status(404).send();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// GET /hap-device/evDevices/
|
|
20
|
+
getDevices(req, res, perms) {
|
|
21
|
+
const devices = this.RED.nodes.getNode(req.params.id)?.evDevices;
|
|
22
|
+
if (devices && devices.length) {
|
|
23
|
+
res.send(devices);
|
|
24
|
+
} else {
|
|
25
|
+
res.status(404).send({ error: `No devices found for perms: ${perms}` });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// GET /hap-device/evDevices/:id
|
|
30
|
+
getDeviceById(req, res, key) {
|
|
31
|
+
const devices = this.RED.nodes.getNode(req.params.id)?.[key];
|
|
32
|
+
if (devices) {
|
|
33
|
+
// debug(`${key} devices`, devices.length);
|
|
34
|
+
res.send(devices);
|
|
35
|
+
} else {
|
|
36
|
+
res.status(404).send();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Register all routes
|
|
41
|
+
registerRoutes() {
|
|
42
|
+
const routes = [
|
|
43
|
+
{ method: 'post', path: '/hap-device/refresh/:id', permission: 'hb-event.read', handler: this.refreshDevice },
|
|
44
|
+
{ method: 'get', path: '/hap-device/evDevices/', permission: 'hb-event.read', handler: (req, res) => this.getDevices(req, res, 'ev') },
|
|
45
|
+
{ method: 'get', path: '/hap-device/evDevices/:id', permission: 'hb-event.read', handler: (req, res) => this.getDeviceById(req, res, 'evDevices') },
|
|
46
|
+
{ method: 'post', path: '/hap-device/refresh/:id', permission: 'hb-resume.read', handler: this.refreshDevice },
|
|
47
|
+
{ method: 'get', path: '/hap-device/evDevices/', permission: 'hb-resume.read', handler: (req, res) => this.getDevices(req, res, 'ev') },
|
|
48
|
+
{ method: 'get', path: '/hap-device/evDevices/:id', permission: 'hb-resume.read', handler: (req, res) => this.getDeviceById(req, res, 'evDevices') },
|
|
49
|
+
{ method: 'get', path: '/hap-device/ctDevices/', permission: 'hb-control.read', handler: (req, res) => this.getDevices(req, res, 'pw') },
|
|
50
|
+
{ method: 'get', path: '/hap-device/ctDevices/:id', permission: 'hb-control.read', handler: (req, res) => this.getDeviceById(req, res, 'ctDevices') },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
routes.forEach(({ method, path, permission, handler }) => {
|
|
54
|
+
this.RED.httpAdmin[method](path, this.RED.auth.needsPermission(permission), handler.bind(this));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = HapDeviceRoutes;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const debug = require('debug')('hapNodeRed:hbBaseNode');
|
|
2
|
+
|
|
3
|
+
class HbBaseNode {
|
|
4
|
+
constructor(config, RED) {
|
|
5
|
+
debug("Constructor:", config.type, JSON.stringify(config));
|
|
6
|
+
RED.nodes.createNode(this, config);
|
|
7
|
+
|
|
8
|
+
if (!config.conf) {
|
|
9
|
+
this.error(`Warning: ${config.type} @ (${config.x}, ${config.y}) not connected to a HB Configuration Node`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.hbConfigNode = RED.nodes.getNode(config.conf);
|
|
14
|
+
this.confId = config.conf;
|
|
15
|
+
this.device = config.device;
|
|
16
|
+
this.service = config.Service;
|
|
17
|
+
this.name = config.name;
|
|
18
|
+
this.fullName = `${config.name} - ${config.Service}`;
|
|
19
|
+
this.hbDevice = null;
|
|
20
|
+
|
|
21
|
+
this.hbConfigNode?.registerClientNode(this);
|
|
22
|
+
|
|
23
|
+
if (this.handleInput) {
|
|
24
|
+
this.on('input', this.handleInput.bind(this));
|
|
25
|
+
}
|
|
26
|
+
if (this.handleHbReady) {
|
|
27
|
+
this.on('hbReady', this.handleHbReady.bind(this))
|
|
28
|
+
}
|
|
29
|
+
this.on('close', this.handleClose.bind(this));
|
|
30
|
+
this.on('hbEvent', this.handleHBEventMessage.bind(this));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
handleHBEventMessage(service) {
|
|
34
|
+
debug('hbEvent for', this.id, this.type, service.serviceName, JSON.stringify(service.values));
|
|
35
|
+
|
|
36
|
+
this.status({
|
|
37
|
+
text: JSON.stringify(service.values),
|
|
38
|
+
shape: 'dot',
|
|
39
|
+
fill: 'green',
|
|
40
|
+
});
|
|
41
|
+
this.send({ payload: service.values });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
createMessage(service) {
|
|
45
|
+
return {
|
|
46
|
+
name: this.name,
|
|
47
|
+
payload: service.values,
|
|
48
|
+
Homebridge: service.instance.name,
|
|
49
|
+
Manufacturer: service.accessoryInformation.Manufacturer,
|
|
50
|
+
Service: service.type,
|
|
51
|
+
_device: this.device,
|
|
52
|
+
_confId: this.confId,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
handleClose(removed, done) {
|
|
57
|
+
debug('close', this.name);
|
|
58
|
+
done();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
statusText(message) {
|
|
62
|
+
return message.slice(0, 20)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
*
|
|
67
|
+
* @param {*} warning - Message to log and display in debug panel
|
|
68
|
+
* @param {*} statusText - Message to display under Node ( If not present, uses warning message text)
|
|
69
|
+
*/
|
|
70
|
+
handleWarning(warning, statusText) {
|
|
71
|
+
this.warn(warning);
|
|
72
|
+
this.status({
|
|
73
|
+
text: (statusText ? statusText : warning).slice(0, 20),
|
|
74
|
+
shape: 'ring',
|
|
75
|
+
fill: 'yellow',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
*
|
|
81
|
+
* @param {*} warning - Message to log and display in debug panel
|
|
82
|
+
* @param {*} statusText - Message to display under Node ( If not present, uses warning message text)
|
|
83
|
+
*/
|
|
84
|
+
handleError(error, statusText) {
|
|
85
|
+
this.error(error);
|
|
86
|
+
this.status({
|
|
87
|
+
text: (statusText ? statusText : error).slice(0, 20),
|
|
88
|
+
shape: 'ring',
|
|
89
|
+
fill: 'red',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = HbBaseNode;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const { HapClient } = require('@homebridge/hap-client');
|
|
2
|
+
const debug = require('debug')('hapNodeRed:hbConfigNode');
|
|
3
|
+
|
|
4
|
+
class HBConfigNode {
|
|
5
|
+
constructor(config, RED) {
|
|
6
|
+
if (!config.jest) {
|
|
7
|
+
RED.nodes.createNode(this, config);
|
|
8
|
+
|
|
9
|
+
// Initialize properties
|
|
10
|
+
this.username = config.username;
|
|
11
|
+
this.macAddress = config.macAddress || '';
|
|
12
|
+
this.users = {};
|
|
13
|
+
this.homebridge = null;
|
|
14
|
+
this.evDevices = [];
|
|
15
|
+
this.ctDevices = [];
|
|
16
|
+
this.hbDevices = [];
|
|
17
|
+
this.clientNodes = [];
|
|
18
|
+
// this.log = new Log(console, true);
|
|
19
|
+
this.discoveryTimeout = null;
|
|
20
|
+
|
|
21
|
+
// Initialize HAP client
|
|
22
|
+
this.hapClient = new HapClient({
|
|
23
|
+
config: { debug: false },
|
|
24
|
+
pin: config.username
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.hapClient.on('instance-discovered', this.waitForNoMoreDiscoveries);
|
|
28
|
+
this.hapClient.on('discovery-ended', this.hapClient.refreshInstances);
|
|
29
|
+
this.waitForNoMoreDiscoveries();
|
|
30
|
+
this.on('close', this.close.bind(this));
|
|
31
|
+
this.refreshInProcess = true; // Prevents multiple refreshes, hapClient kicks of a discovery on start
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Start device discovery after monitor reports issues
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
refreshDevices = () => {
|
|
40
|
+
if (!this.refreshInProcess) {
|
|
41
|
+
|
|
42
|
+
this.monitor.finish();
|
|
43
|
+
this.debug('Monitor reported homebridge stability issues, refreshing devices');
|
|
44
|
+
this.hapClient.on('instance-discovered', this.waitForNoMoreDiscoveries);
|
|
45
|
+
this.hapClient.resetInstancePool();
|
|
46
|
+
this.waitForNoMoreDiscoveries();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Wait for no more instance discoveries to be made before publishing services
|
|
52
|
+
*/
|
|
53
|
+
waitForNoMoreDiscoveries = () => {
|
|
54
|
+
if (!this.discoveryTimeout) {
|
|
55
|
+
clearTimeout(this.discoveryTimeout);
|
|
56
|
+
this.discoveryTimeout = setTimeout(() => {
|
|
57
|
+
this.debug('No more instances discovered, publishing services');
|
|
58
|
+
this.hapClient.removeListener('instance-discovered', this.waitForNoMoreDiscoveries);
|
|
59
|
+
this.handleReady();
|
|
60
|
+
this.discoveryTimeout = null;
|
|
61
|
+
this.refreshInProcess = false;
|
|
62
|
+
}, 20000); // resetInstancePool() triggers a discovery after 6 seconds. Need to wait for it to finish.
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Populate the list of devices and handle duplicates
|
|
68
|
+
*/
|
|
69
|
+
async handleReady() {
|
|
70
|
+
const updatedDevices = await this.hapClient.getAllServices();
|
|
71
|
+
updatedDevices.forEach((updatedService, index) => {
|
|
72
|
+
if (this.hbDevices.find(service => service.uniqueId === updatedService.uniqueId)) {
|
|
73
|
+
const update = this.hbDevices.find(service => service.uniqueId === updatedService.uniqueId);
|
|
74
|
+
update.instance = updatedService.instance;
|
|
75
|
+
} else {
|
|
76
|
+
this.hbDevices.push(updatedService);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
this.evDevices = this.toList({ perms: 'ev' });
|
|
80
|
+
this.ctDevices = this.toList({ perms: 'pw' });
|
|
81
|
+
this.log(`Devices initialized: evDevices: ${this.evDevices.length}, ctDevices: ${this.ctDevices.length}`);
|
|
82
|
+
this.handleDuplicates(this.evDevices);
|
|
83
|
+
this.connectClientNodes();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
toList(perms) {
|
|
87
|
+
const supportedTypes = new Set([
|
|
88
|
+
'Battery', 'Carbon Dioxide Sensor', 'Carbon Monoxide Sensor', 'Camera Rtp Stream Management',
|
|
89
|
+
'Doorbell', 'Fan', 'Fanv2', 'Garage Door Opener', 'Humidity Sensor', 'Input Source',
|
|
90
|
+
'Leak Sensor', 'Lightbulb', 'Lock Mechanism', 'Motion Sensor', 'Occupancy Sensor',
|
|
91
|
+
'Outlet', 'Smoke Sensor', 'Speaker', 'Stateless Programmable Switch', 'Switch',
|
|
92
|
+
'Television', 'Temperature Sensor', 'Thermostat', 'Contact Sensor',
|
|
93
|
+
]);
|
|
94
|
+
return filterUnique(this.hbDevices)
|
|
95
|
+
.filter(service => supportedTypes.has(service.humanType))
|
|
96
|
+
.map(service => ({
|
|
97
|
+
name: service.serviceName,
|
|
98
|
+
fullName: `${service.serviceName} - ${service.type}`,
|
|
99
|
+
sortName: `${service.serviceName}:${service.type}`,
|
|
100
|
+
uniqueId: `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}`,
|
|
101
|
+
homebridge: service.instance.name,
|
|
102
|
+
service: service.type,
|
|
103
|
+
manufacturer: service.accessoryInformation.Manufacturer,
|
|
104
|
+
}))
|
|
105
|
+
.sort((a, b) => a.sortName.localeCompare(b.sortName));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
handleDuplicates(list) {
|
|
110
|
+
const seen = new Map();
|
|
111
|
+
|
|
112
|
+
list.forEach(endpoint => {
|
|
113
|
+
const { fullName, uniqueId } = endpoint;
|
|
114
|
+
|
|
115
|
+
if (seen.has(fullName)) {
|
|
116
|
+
this.warn(`Duplicate device name detected: ${fullName}`);
|
|
117
|
+
}
|
|
118
|
+
if (seen.has(uniqueId)) {
|
|
119
|
+
this.error(`Duplicate uniqueId detected: ${uniqueId}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
seen.set(fullName, true);
|
|
123
|
+
seen.set(uniqueId, true);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
registerClientNode(clientNode) {
|
|
128
|
+
debug('Register: %s type: %s', clientNode.type, clientNode.name);
|
|
129
|
+
this.clientNodes[clientNode.id] = clientNode;
|
|
130
|
+
clientNode.status({ fill: 'yellow', shape: 'ring', text: 'connecting' });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async connectClientNodes() {
|
|
134
|
+
debug('connect %s nodes', Object.keys(this.clientNodes).length);
|
|
135
|
+
for (const [key, clientNode] of Object.entries(this.clientNodes)) {
|
|
136
|
+
// debug('_Register: %s type: %s', clientNode.type, clientNode.name, clientNode.instance);
|
|
137
|
+
const matchedDevice = this.hbDevices.find(service =>
|
|
138
|
+
clientNode.device === `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}`
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (matchedDevice) {
|
|
142
|
+
clientNode.hbDevice = matchedDevice;
|
|
143
|
+
clientNode.status({ fill: 'green', shape: 'dot', text: 'connected' });
|
|
144
|
+
clientNode.emit('hbReady', matchedDevice);
|
|
145
|
+
debug('_Registered: %s type: %s', clientNode.type, matchedDevice.type, matchedDevice.serviceName);
|
|
146
|
+
} else {
|
|
147
|
+
this.error(`ERROR: Device registration failed ${clientNode.name}`);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
await this.monitorDevices();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async monitorDevices() {
|
|
155
|
+
if (Object.keys(this.clientNodes).length) {
|
|
156
|
+
const uniqueDevices = new Set();
|
|
157
|
+
|
|
158
|
+
const monitorNodes = Object.values(this.clientNodes)
|
|
159
|
+
.filter(node => ['hb-status', 'hb-control', 'hb-event', 'hb-resume'].includes(node.type)) // Filter by type
|
|
160
|
+
.filter(node => {
|
|
161
|
+
if (uniqueDevices.has(node.device)) {
|
|
162
|
+
return false; // Exclude duplicates
|
|
163
|
+
}
|
|
164
|
+
uniqueDevices.add(node.device);
|
|
165
|
+
return true; // Include unique devices
|
|
166
|
+
})
|
|
167
|
+
.map(node => node.hbDevice) // Map to hbDevice property
|
|
168
|
+
.filter(Boolean); // Remove any undefined or null values, if present;
|
|
169
|
+
debug('monitorNodes', Object.keys(monitorNodes).length);
|
|
170
|
+
// console.log('monitorNodes', monitorNodes);
|
|
171
|
+
this.monitor = await this.hapClient.monitorCharacteristics(monitorNodes);
|
|
172
|
+
this.monitor.on('service-update', (services) => {
|
|
173
|
+
services.forEach(service => {
|
|
174
|
+
const eventNodes = Object.values(this.clientNodes).filter(clientNode =>
|
|
175
|
+
clientNode.config.device === `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}`
|
|
176
|
+
);
|
|
177
|
+
// debug('service-update', service.serviceName, eventNodes);
|
|
178
|
+
eventNodes.forEach(eventNode => eventNode.emit('hbEvent', service));
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
this.monitor.on('monitor-close', (instance, hadError) => {
|
|
182
|
+
debug('monitor-close', instance.name, instance.ipAddress, instance.port, hadError)
|
|
183
|
+
this.disconnectClientNodes(instance);
|
|
184
|
+
// this.refreshDevices();
|
|
185
|
+
})
|
|
186
|
+
this.monitor.on('monitor-refresh', (instance, hadError) => {
|
|
187
|
+
debug('monitor-refresh', instance.name, instance.ipAddress, instance.port, hadError)
|
|
188
|
+
this.reconnectClientNodes(instance);
|
|
189
|
+
// this.refreshDevices();
|
|
190
|
+
})
|
|
191
|
+
this.monitor.on('monitor-error', (instance, hadError) => {
|
|
192
|
+
debug('monitor-error', instance, hadError)
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
disconnectClientNodes(instance) {
|
|
198
|
+
debug('disconnectClientNodes', `${instance.ipAddress}:${instance.port}`);
|
|
199
|
+
const clientNodes = Object.values(this.clientNodes).filter(clientNode => {
|
|
200
|
+
return `${clientNode.hbDevice?.instance.ipAddress}:${clientNode.hbDevice?.instance.port}` === `${instance.ipAddress}:${instance.port}`;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
clientNodes.forEach(clientNode => {
|
|
204
|
+
clientNode.status({ fill: 'red', shape: 'ring', text: 'disconnected' });
|
|
205
|
+
clientNode.emit('hbDisconnected', instance);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
reconnectClientNodes(instance) {
|
|
210
|
+
debug('reconnectClientNodes', `${instance.ipAddress}:${instance.port}`);
|
|
211
|
+
const clientNodes = Object.values(this.clientNodes).filter(clientNode => {
|
|
212
|
+
return `${clientNode.hbDevice?.instance.ipAddress}:${clientNode.hbDevice?.instance.port}` === `${instance.ipAddress}:${instance.port}`;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
clientNodes.forEach(clientNode => {
|
|
216
|
+
clientNode.status({ fill: 'green', shape: 'dot', text: 'connected' });
|
|
217
|
+
clientNode.emit('hbReady', clientNode.hbDevice);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
close() {
|
|
222
|
+
debug('hb-config: close');
|
|
223
|
+
this.hapClient?.destroy();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
// Filter unique devices by AID, service name, username, and port
|
|
229
|
+
const filterUnique = (data) => {
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
return data.filter(item => {
|
|
232
|
+
const uniqueKey = `${item.aid}-${item.serviceName}-${item.humanType}-${item.instance.username}-${item.instance.port}`;
|
|
233
|
+
if (seen.has(uniqueKey)) return false;
|
|
234
|
+
seen.add(uniqueKey);
|
|
235
|
+
return true;
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
module.exports = HBConfigNode;
|