homebridge-openwrt-control 0.0.2-beta.9 → 0.0.2
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 +8 -0
- package/config.schema.json +57 -11
- package/index.js +70 -63
- package/package.json +4 -3
- package/src/{apdevice.js → accesspoint.js} +62 -62
- package/src/mqtt.js +103 -0
- package/src/openwrt.js +102 -93
- package/src/restful.js +87 -0
- package/src/{swdevice.js → switch.js} +64 -62
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## Warning
|
|
9
9
|
|
|
10
|
+
## [0.0.2] - (17.01.2026)
|
|
11
|
+
|
|
12
|
+
## Changes
|
|
13
|
+
|
|
14
|
+
- added RESTFul and MQTT external integration (work in progress)
|
|
15
|
+
- refactor and optimizations
|
|
16
|
+
- cleanup
|
|
17
|
+
|
|
10
18
|
## [0.0.1] - (16.01.2026)
|
|
11
19
|
|
|
12
20
|
## Changes
|
package/config.schema.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"singular": true,
|
|
5
5
|
"fixArrays": true,
|
|
6
6
|
"strictValidation": true,
|
|
7
|
-
"headerDisplay": "This plugin works with OpenWrt
|
|
7
|
+
"headerDisplay": "This plugin works with OpenWrt flashed devices. Devices are exposed to HomeKit as separate accessories and each needs to be manually paired.",
|
|
8
8
|
"footerDisplay": "For documentation please see [GitHub repository](https://github.com/grzegorz914/homebridge-openwrt-control).",
|
|
9
9
|
"schema": {
|
|
10
10
|
"type": "object",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"type": "array",
|
|
14
14
|
"items": {
|
|
15
15
|
"type": "object",
|
|
16
|
-
"title": "
|
|
16
|
+
"title": "Device",
|
|
17
17
|
"properties": {
|
|
18
18
|
"name": {
|
|
19
19
|
"title": "Name",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
}
|
|
97
97
|
]
|
|
98
98
|
},
|
|
99
|
-
"
|
|
99
|
+
"apDevice": {
|
|
100
100
|
"title": "Access point",
|
|
101
101
|
"type": "object",
|
|
102
102
|
"properties": {
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"placeholder": "Name",
|
|
112
112
|
"description": "Here set the Name to be displayed in Homebridge/HomeKit for this access point.",
|
|
113
113
|
"condition": {
|
|
114
|
-
"functionBody": "return model.devices[arrayIndices].
|
|
114
|
+
"functionBody": "return model.devices[arrayIndices].apDevice.enable === true;"
|
|
115
115
|
}
|
|
116
116
|
},
|
|
117
117
|
"namePrefix": {
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"type": "boolean",
|
|
120
120
|
"description": "Here enable/disable the accessory name as a prefix for the access point name.",
|
|
121
121
|
"condition": {
|
|
122
|
-
"functionBody": "return model.devices[arrayIndices].
|
|
122
|
+
"functionBody": "return model.devices[arrayIndices].apDevice.enable === true;"
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
"sensor": {
|
|
@@ -127,7 +127,43 @@
|
|
|
127
127
|
"type": "boolean",
|
|
128
128
|
"description": "Here Enable/Disable sensor for every SSID.",
|
|
129
129
|
"condition": {
|
|
130
|
-
"functionBody": "return model.devices[arrayIndices].
|
|
130
|
+
"functionBody": "return model.devices[arrayIndices].apDevice.enable === true;"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"swDevice": {
|
|
136
|
+
"title": "Access point",
|
|
137
|
+
"type": "object",
|
|
138
|
+
"properties": {
|
|
139
|
+
"enable": {
|
|
140
|
+
"title": "Enable",
|
|
141
|
+
"type": "boolean",
|
|
142
|
+
"description": "Here Enable/Disable this switch control."
|
|
143
|
+
},
|
|
144
|
+
"name": {
|
|
145
|
+
"title": "Name",
|
|
146
|
+
"type": "string",
|
|
147
|
+
"placeholder": "Name",
|
|
148
|
+
"description": "Here set the Name to be displayed in Homebridge/HomeKit for this switch.",
|
|
149
|
+
"condition": {
|
|
150
|
+
"functionBody": "return model.devices[arrayIndices].swDevice.enable === true;"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"namePrefix": {
|
|
154
|
+
"title": "Prefix",
|
|
155
|
+
"type": "boolean",
|
|
156
|
+
"description": "Here enable/disable the accessory name as a prefix for the port name.",
|
|
157
|
+
"condition": {
|
|
158
|
+
"functionBody": "return model.devices[arrayIndices].swDevice.enable === true;"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"sensor": {
|
|
162
|
+
"title": "Sensor",
|
|
163
|
+
"type": "boolean",
|
|
164
|
+
"description": "Here Enable/Disable sensor for every port.",
|
|
165
|
+
"condition": {
|
|
166
|
+
"functionBody": "return model.devices[arrayIndices].swDevice.enable === true;"
|
|
131
167
|
}
|
|
132
168
|
}
|
|
133
169
|
}
|
|
@@ -369,13 +405,23 @@
|
|
|
369
405
|
"title": "{{ value.title }}",
|
|
370
406
|
"items": [
|
|
371
407
|
{
|
|
372
|
-
"key": "devices[].
|
|
408
|
+
"key": "devices[].apDevice",
|
|
373
409
|
"title": "Access Point",
|
|
374
410
|
"items": [
|
|
375
|
-
"devices[].
|
|
376
|
-
"devices[].
|
|
377
|
-
"devices[].
|
|
378
|
-
"devices[].
|
|
411
|
+
"devices[].apDevice.enable",
|
|
412
|
+
"devices[].apDevice.name",
|
|
413
|
+
"devices[].apDevice.namePrefix",
|
|
414
|
+
"devices[].apDevice.sensor"
|
|
415
|
+
]
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
"key": "devices[].swDevice",
|
|
419
|
+
"title": "Switch",
|
|
420
|
+
"items": [
|
|
421
|
+
"devices[].swDevice.enable",
|
|
422
|
+
"devices[].swDevice.name",
|
|
423
|
+
"devices[].swDevice.namePrefix",
|
|
424
|
+
"devices[].swDevice.sensor"
|
|
379
425
|
]
|
|
380
426
|
},
|
|
381
427
|
{
|
package/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { mkdirSync, existsSync, writeFileSync } from 'fs';
|
|
3
3
|
import OpenWrt from './src/openwrt.js';
|
|
4
|
-
import AccessPoint from './src/
|
|
5
|
-
import Switch from './src/
|
|
4
|
+
import AccessPoint from './src/accesspoint.js';
|
|
5
|
+
import Switch from './src/switch.js';
|
|
6
6
|
import ImpulseGenerator from './src/impulsegenerator.js';
|
|
7
7
|
import { PluginName, PlatformName } from './src/constants.js';
|
|
8
8
|
|
|
@@ -14,7 +14,6 @@ class OpenWrtPlatform {
|
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
this.accessories = [];
|
|
17
|
-
this.devices = [];
|
|
18
17
|
|
|
19
18
|
//check if prefs directory exist
|
|
20
19
|
const prefDir = join(api.user.storagePath(), 'openWrt');
|
|
@@ -26,34 +25,43 @@ class OpenWrtPlatform {
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
api.on('didFinishLaunching', async () => {
|
|
29
|
-
for (const
|
|
30
|
-
const { name, host, displayType } =
|
|
28
|
+
for (const device of config.devices) {
|
|
29
|
+
const { name, host, displayType } = device;
|
|
31
30
|
if (!name || !host || !displayType) {
|
|
32
|
-
log.warn(`Device: ${host || 'host missing'}, ${name || 'name missing'}, ${!displayType ? ',
|
|
31
|
+
log.warn(`Device: ${host || 'host missing'}, ${name || 'name missing'}, ${!displayType ? ', display type disabled' : ''} in config, will not be published in the Home app`);
|
|
33
32
|
continue;
|
|
34
33
|
}
|
|
34
|
+
const apDevice = device.apDevice || {};
|
|
35
|
+
const swDevice = device.swDevice || {};
|
|
36
|
+
const refreshInterval = (device.refreshInterval ?? 5) * 1000;
|
|
37
|
+
|
|
38
|
+
//check enabled devices
|
|
39
|
+
const configuredDevices = [];
|
|
40
|
+
if (apDevice.enable) configuredDevices.push(0);
|
|
41
|
+
if (swDevice.enable) configuredDevices.push(1);
|
|
42
|
+
if (configuredDevices.length === 0) continue;
|
|
35
43
|
|
|
36
44
|
//log config
|
|
37
45
|
const logLevel = {
|
|
38
|
-
devInfo:
|
|
39
|
-
success:
|
|
40
|
-
info:
|
|
41
|
-
warn:
|
|
42
|
-
error:
|
|
43
|
-
debug:
|
|
46
|
+
devInfo: device.log?.deviceInfo,
|
|
47
|
+
success: device.log?.success,
|
|
48
|
+
info: device.log?.info,
|
|
49
|
+
warn: device.log?.warn,
|
|
50
|
+
error: device.log?.error,
|
|
51
|
+
debug: device.log?.debug
|
|
44
52
|
};
|
|
45
53
|
|
|
46
54
|
if (logLevel.debug) {
|
|
47
55
|
log.info(`Device: ${host} ${name}, did finish launching.`);
|
|
48
56
|
const safeConfig = {
|
|
49
|
-
...
|
|
57
|
+
...device,
|
|
50
58
|
auth: {
|
|
51
|
-
...
|
|
59
|
+
...device.auth,
|
|
52
60
|
passwd: 'removed',
|
|
53
61
|
},
|
|
54
62
|
mqtt: {
|
|
55
63
|
auth: {
|
|
56
|
-
...
|
|
64
|
+
...device.mqtt?.auth,
|
|
57
65
|
passwd: 'removed',
|
|
58
66
|
}
|
|
59
67
|
},
|
|
@@ -61,75 +69,74 @@ class OpenWrtPlatform {
|
|
|
61
69
|
log.info(`Device: ${host} ${name}, Config: ${JSON.stringify(safeConfig, null, 2)}`);
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
const refreshInterval = (deviceConfig.refreshInterval ?? 5) * 1000;
|
|
65
|
-
if (deviceConfig.accessPoint?.enable) this.devices.push('accessPoint');
|
|
66
|
-
if (deviceConfig.switch?.enable) this.devices.push('switch');
|
|
67
|
-
|
|
68
|
-
if (this.devices.length === 0) return;
|
|
69
|
-
|
|
70
|
-
const openWrt = new OpenWrt(deviceConfig)
|
|
71
|
-
.on('success', msg => logLevel.success && log.success(`Device: ${host}, ${msg}`))
|
|
72
|
-
.on('info', msg => log.info(`Device: ${host}, ${msg}`))
|
|
73
|
-
.on('debug', msg => log.info(`Device: ${host}, debug: ${msg}`))
|
|
74
|
-
.on('warn', msg => log.warn(`Device: ${host}, ${msg}`))
|
|
75
|
-
.on('error', msg => log.error(`Device: ${host}, ${msg}`))
|
|
76
|
-
|
|
77
|
-
const openWrtInfo = await openWrt.connect();
|
|
78
|
-
if (!openWrtInfo.state) {
|
|
79
|
-
if (logLevel.warn) log.warn(`Device: ${host} ${name}, no data received`);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
72
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
73
|
+
// create impulse generator for every device
|
|
74
|
+
const impulseGenerator = new ImpulseGenerator()
|
|
75
|
+
.on('start', async () => {
|
|
76
|
+
try {
|
|
77
|
+
|
|
78
|
+
const openWrt = new OpenWrt(device)
|
|
79
|
+
.on('success', msg => logLevel.success && log.success(`Device: ${host}, ${msg}`))
|
|
80
|
+
.on('info', msg => log.info(`Device: ${host} ${name}, ${msg}`))
|
|
81
|
+
.on('debug', msg => log.info(`Device: ${host} ${name}, debug: ${msg}`))
|
|
82
|
+
.on('warn', msg => log.warn(`Device: ${host} ${name}, ${msg}`))
|
|
83
|
+
.on('error', msg => log.error(`Device: ${host} ${name}, ${msg}`));
|
|
84
|
+
|
|
85
|
+
const openWrtInfo = await openWrt.connect();
|
|
86
|
+
if (!openWrtInfo.state) {
|
|
87
|
+
if (logLevel.warn) log.warn(`Device: ${host} ${name}, ${openWrtInfo.info}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (logLevel.success) log.success(`Device: ${host} ${name}, ${openWrtInfo.info}`);
|
|
91
|
+
|
|
92
|
+
// start openwrt impulse generator
|
|
93
|
+
await openWrt.impulseGenerator.state(true, [{ name: 'connect', sampling: refreshInterval }], false);
|
|
94
|
+
|
|
95
|
+
for (const deviceType of configuredDevices) {
|
|
96
|
+
let configuredDevice;
|
|
97
|
+
switch (deviceType) {
|
|
98
|
+
case 0:
|
|
99
|
+
configuredDevice = new AccessPoint(api, device, openWrt, openWrtInfo);
|
|
100
|
+
break;
|
|
101
|
+
case 1:
|
|
102
|
+
configuredDevice = new Switch(api, device, openWrt, openWrtInfo);
|
|
103
|
+
break;
|
|
95
104
|
default:
|
|
96
|
-
if (logLevel.warn) log.warn(`Device: ${host} ${name},
|
|
105
|
+
if (logLevel.warn) log.warn(`Device: ${host} ${name}, class not found for: ${deviceType}`);
|
|
97
106
|
return;
|
|
98
107
|
}
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
.on('devInfo', msg => logLevel.devInfo && log.info(msg))
|
|
109
|
+
configuredDevice.on('devInfo', msg => logLevel.devInfo && log.info(msg))
|
|
102
110
|
.on('success', msg => logLevel.success && log.success(`Device: ${host} ${name}, ${msg}`))
|
|
103
111
|
.on('info', msg => log.info(`Device: ${host} ${name}, ${msg}`))
|
|
104
112
|
.on('debug', msg => log.info(`Device: ${host} ${name}, debug: ${msg}`))
|
|
105
113
|
.on('warn', msg => log.warn(`Device: ${host} ${name}, ${msg}`))
|
|
106
114
|
.on('error', msg => log.error(`Device: ${host} ${name}, ${msg}`));
|
|
107
115
|
|
|
108
|
-
const accessory = await
|
|
116
|
+
const accessory = await configuredDevice.start();
|
|
109
117
|
if (accessory) {
|
|
110
118
|
api.publishExternalAccessories(PluginName, [accessory]);
|
|
111
119
|
if (logLevel.success) log.success(`Device: ${host} ${name}, Published as external accessory.`);
|
|
112
120
|
}
|
|
113
|
-
|
|
114
|
-
// stop master impulse generator
|
|
115
|
-
await impulseGenerator.state(false);
|
|
116
|
-
} catch (error) {
|
|
117
|
-
if (logLevel.error) log.error(`Device: ${host} ${name}, Start impulse generator error: ${error.message ?? error}, trying again.`);
|
|
118
121
|
}
|
|
119
|
-
})
|
|
120
|
-
.on('state', (state) => {
|
|
121
|
-
if (logLevel.debug) log.info(`Device: ${host} ${name}, Start impulse generator ${state ? 'started' : 'stopped'}.`);
|
|
122
|
-
});
|
|
123
122
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
// stop accessory impulse generator
|
|
124
|
+
await impulseGenerator.state(false);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (logLevel.error) log.error(`Device: ${host} ${name}, Start impulse generator error: ${error.message ?? error}, trying again.`);
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
.on('state', (state) => {
|
|
130
|
+
if (logLevel.debug) log.info(`Device: ${host} ${name}, Start impulse generator ${state ? 'started' : 'stopped'}.`);
|
|
131
|
+
});
|
|
127
132
|
|
|
128
|
-
// start
|
|
129
|
-
await
|
|
133
|
+
// start accessory impulse generator
|
|
134
|
+
await impulseGenerator.state(true, [{ name: 'start', sampling: 120000 }]);
|
|
130
135
|
} catch (error) {
|
|
131
136
|
if (logLevel.error) log.error(`Device: ${host} ${name}, Did finish launching error: ${error.message ?? error}`);
|
|
132
137
|
}
|
|
138
|
+
|
|
139
|
+
await new Promise(r => setTimeout(r, 500));
|
|
133
140
|
}
|
|
134
141
|
});
|
|
135
142
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "OpenWrt Control",
|
|
3
3
|
"name": "homebridge-openwrt-control",
|
|
4
|
-
"version": "0.0.2
|
|
4
|
+
"version": "0.0.2",
|
|
5
5
|
"description": "Homebridge plugin to control OpenWrt flashed devices.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "grzegorz914",
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
"node": "^20 || ^22 || ^24 || ^25"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"
|
|
37
|
+
"mqtt": "^5.14.1",
|
|
38
|
+
"axios": "^1.13.2",
|
|
39
|
+
"express": "^5.2.1"
|
|
38
40
|
},
|
|
39
41
|
"keywords": [
|
|
40
42
|
"homebridge",
|
|
@@ -44,7 +46,6 @@
|
|
|
44
46
|
"accesspoint",
|
|
45
47
|
"router",
|
|
46
48
|
"switch"
|
|
47
|
-
|
|
48
49
|
],
|
|
49
50
|
"contributors": [],
|
|
50
51
|
"scripts": {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import EventEmitter from 'events';
|
|
2
|
+
import RestFul from './restful.js';
|
|
3
|
+
import Mqtt from './mqtt.js';
|
|
2
4
|
let Accessory, Characteristic, Service, Categories, AccessoryUUID;
|
|
3
5
|
|
|
4
|
-
class
|
|
6
|
+
class AccessPoint extends EventEmitter {
|
|
5
7
|
constructor(api, config, openWrt, openWrtInfo) {
|
|
6
8
|
super();
|
|
7
9
|
|
|
@@ -12,8 +14,12 @@ class AccessPointDevice extends EventEmitter {
|
|
|
12
14
|
AccessoryUUID = api.hap.uuid;
|
|
13
15
|
|
|
14
16
|
//config
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.host = config.host;
|
|
19
|
+
this.name = config.apDevice?.name || openWrtInfo.systemInfo.hostname;
|
|
20
|
+
this.namePrefix = config.apDevice?.namePrefix || false;
|
|
21
|
+
this.sensorsEnabled = config.apDevice?.sensor || false
|
|
22
|
+
this.logDeviceInfo = config.log?.deviceInfo || false;
|
|
17
23
|
this.logInfo = config.log?.info || false;
|
|
18
24
|
this.logDebug = config.log?.debug || false;
|
|
19
25
|
|
|
@@ -26,7 +32,37 @@ class AccessPointDevice extends EventEmitter {
|
|
|
26
32
|
//openwrt
|
|
27
33
|
this.openWrt = openWrt;
|
|
28
34
|
this.openWrtInfo = openWrtInfo;
|
|
29
|
-
|
|
35
|
+
|
|
36
|
+
//openwrt client
|
|
37
|
+
openWrt.on('openWrtInfo', (openWrtInfo) => {
|
|
38
|
+
this.informationService?.updateCharacteristic(Characteristic.FirmwareRevision, openWrtInfo.systemInfo.release?.version);
|
|
39
|
+
|
|
40
|
+
// update state
|
|
41
|
+
const ssids = openWrtInfo.ssids;
|
|
42
|
+
for (let i = 0; i < ssids.length; i++) {
|
|
43
|
+
const ssid = ssids[i];
|
|
44
|
+
const name = ssid.name;
|
|
45
|
+
const state = ssid.state;
|
|
46
|
+
const serviceName = this.namePrefix ? `${this.name} ${name}` : name;
|
|
47
|
+
this.services?.[i]
|
|
48
|
+
?.setCharacteristic(Characteristic.ConfiguredName, serviceName)
|
|
49
|
+
.updateCharacteristic(Characteristic.On, state);
|
|
50
|
+
|
|
51
|
+
this.sensorServices?.[i]
|
|
52
|
+
?.setCharacteristic(Characteristic.ConfiguredName, serviceName)
|
|
53
|
+
.updateCharacteristic(Characteristic.ContactSensorState, state);
|
|
54
|
+
|
|
55
|
+
if (this.logInfo) {
|
|
56
|
+
this.emit('info', `Name: ${ssid.name}`);
|
|
57
|
+
this.emit('info', `State: ${ssid.state}`);
|
|
58
|
+
this.emit('info', `Mode: ${ssid.mode}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//restFul and mqtt
|
|
63
|
+
if (this.restFulConnected) this.emit('restFul', 'info', openWrtInfo);
|
|
64
|
+
if (this.mqttConnected) this.emit('mqtt', 'Info', openWrtInfo);
|
|
65
|
+
});
|
|
30
66
|
};
|
|
31
67
|
|
|
32
68
|
async externalIntegrations() {
|
|
@@ -65,8 +101,8 @@ class AccessPointDevice extends EventEmitter {
|
|
|
65
101
|
this.mqtt1 = new Mqtt({
|
|
66
102
|
host: this.mqtt.host,
|
|
67
103
|
port: this.mqtt.port || 1883,
|
|
68
|
-
clientId: this.mqtt.clientId ? `${this.
|
|
69
|
-
prefix: this.mqtt.prefix ? `${this.
|
|
104
|
+
clientId: this.mqtt.clientId ? `${this.openWrtInfo.systemInfo.release.distribution || 'OpenWrt'}_${this.mqtt.clientId}_${Math.random().toString(16).slice(3)}` : `${this.openWrtInfo.systemInfo.release.distribution || 'OpenWrt'}_${Math.random().toString(16).slice(3)}`,
|
|
105
|
+
prefix: this.mqtt.prefix ? `${this.openWrtInfo.systemInfo.release.distribution || 'OpenWrt'}/${this.mqtt.prefix}/${this.name}` : `${this.openWrtInfo.systemInfo.release.distribution || 'OpenWrt'}/${this.name}`,
|
|
70
106
|
user: this.mqtt.auth?.user,
|
|
71
107
|
passwd: this.mqtt.auth?.passwd,
|
|
72
108
|
logWarn: this.logWarn,
|
|
@@ -121,28 +157,28 @@ class AccessPointDevice extends EventEmitter {
|
|
|
121
157
|
//prepare accessory
|
|
122
158
|
if (this.logDebug) this.emit('debug', `prepare accessory`);
|
|
123
159
|
const accessoryName = this.name;
|
|
124
|
-
const accessoryUUID = AccessoryUUID.generate(this.openWrtInfo.systemInfo.
|
|
160
|
+
const accessoryUUID = AccessoryUUID.generate(this.host + this.openWrtInfo.systemInfo.system);
|
|
125
161
|
const accessoryCategory = Categories.AIRPORT;
|
|
126
162
|
const accessory = new Accessory(accessoryName, accessoryUUID, accessoryCategory);
|
|
127
163
|
|
|
128
164
|
//prepare information service
|
|
129
165
|
if (this.logDebug) this.emit('debug', `prepare information service`);
|
|
130
|
-
accessory.getService(Service.AccessoryInformation)
|
|
131
|
-
.setCharacteristic(Characteristic.Manufacturer, 'OpenWrt')
|
|
166
|
+
this.informationService = accessory.getService(Service.AccessoryInformation)
|
|
167
|
+
.setCharacteristic(Characteristic.Manufacturer, this.openWrtInfo.systemInfo.release.distribution || 'OpenWrt')
|
|
132
168
|
.setCharacteristic(Characteristic.Model, this.openWrtInfo.systemInfo.model)
|
|
133
169
|
.setCharacteristic(Characteristic.SerialNumber, this.openWrtInfo.systemInfo.system)
|
|
134
|
-
.setCharacteristic(Characteristic.FirmwareRevision, this.openWrtInfo.systemInfo.release?.
|
|
135
|
-
.setCharacteristic(Characteristic.ConfiguredName, accessoryName);
|
|
170
|
+
.setCharacteristic(Characteristic.FirmwareRevision, this.openWrtInfo.systemInfo.release?.version);
|
|
136
171
|
|
|
137
172
|
if (this.logDebug) this.emit('debug', `prepare service`);
|
|
138
173
|
|
|
139
|
-
//
|
|
174
|
+
//services
|
|
140
175
|
this.services = [];
|
|
141
|
-
|
|
176
|
+
this.sensorServices = [];
|
|
177
|
+
for (const ssid of this.openWrtInfo.ssids) {
|
|
142
178
|
const name = ssid.name;
|
|
143
179
|
if (this.logDebug) this.emit('debug', `prepare ssid: ${name} service`);
|
|
144
180
|
|
|
145
|
-
const serviceName = this.
|
|
181
|
+
const serviceName = this.namePrefix ? `${accessoryName} ${name}` : name;
|
|
146
182
|
const service = accessory.addService(Service.Switch, serviceName, `service${name}`);
|
|
147
183
|
service.addOptionalCharacteristic(Characteristic.ConfiguredName);
|
|
148
184
|
service.setCharacteristic(Characteristic.ConfiguredName, serviceName);
|
|
@@ -163,10 +199,8 @@ class AccessPointDevice extends EventEmitter {
|
|
|
163
199
|
});
|
|
164
200
|
this.services.push(service);
|
|
165
201
|
|
|
166
|
-
if (this.
|
|
202
|
+
if (this.sensorsEnabled) {
|
|
167
203
|
if (this.logDebug) this.emit('debug', `prepare ssid: ${name} sensor service`);
|
|
168
|
-
|
|
169
|
-
this.sensorServices = [];
|
|
170
204
|
const sensorService = accessory.addService(Service.ContactSensor, serviceName, `sensorService${name}`);
|
|
171
205
|
sensorService.addOptionalCharacteristic(Characteristic.ConfiguredName);
|
|
172
206
|
sensorService.setCharacteristic(Characteristic.ConfiguredName, serviceName);
|
|
@@ -191,50 +225,16 @@ class AccessPointDevice extends EventEmitter {
|
|
|
191
225
|
//start external integrations
|
|
192
226
|
if (this.restFul.enable || this.mqtt.enable) await this.externalIntegrations();
|
|
193
227
|
|
|
194
|
-
this.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
})
|
|
205
|
-
.on('wirelessStatus', async (status) => {
|
|
206
|
-
})
|
|
207
|
-
.on('wirelessRadios', async (radios) => {
|
|
208
|
-
})
|
|
209
|
-
.on('ssids', async (ssids) => {
|
|
210
|
-
// sensors
|
|
211
|
-
for (let i = 0; i < ssids.length; i++) {
|
|
212
|
-
const ssid = ssids[i];
|
|
213
|
-
|
|
214
|
-
const name = ssid[i].name;
|
|
215
|
-
const state = ssid[i].state;
|
|
216
|
-
const serviceName = this.accessPoint.namePrefix ? `${this.name} ${name}` : name;
|
|
217
|
-
this.services?.[i]
|
|
218
|
-
?.setCharacteristic(Characteristic.ConfiguredName, serviceName)
|
|
219
|
-
.updateCharacteristic(Characteristic.On, state);
|
|
220
|
-
|
|
221
|
-
this.sensorServices?.[i]
|
|
222
|
-
?.setCharacteristic(Characteristic.ConfiguredName, name)
|
|
223
|
-
.updateCharacteristic(Characteristic.ContactSensorState, state ? 0 : 1);
|
|
224
|
-
|
|
225
|
-
if (this.logInfo) {
|
|
226
|
-
this.emit('info', `SSID name: ${ssid.name}`);
|
|
227
|
-
this.emit('info', `Name: ${ssid.state}`);
|
|
228
|
-
this.emit('info', `Mode: ${ssid.mode}`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
})
|
|
232
|
-
.on('restFul', (path, data) => {
|
|
233
|
-
if (this.restFulConnected) this.restFul1.update(path, data);
|
|
234
|
-
})
|
|
235
|
-
.on('mqtt', (topic, message) => {
|
|
236
|
-
if (this.mqttConnected) this.mqtt1.emit('publish', topic, message);
|
|
237
|
-
});
|
|
228
|
+
if (this.logDeviceInfo) {
|
|
229
|
+
this.emit('devInfo', `-------- Access Point ${this.name} --------`);
|
|
230
|
+
this.emit('devInfo', `Model: ${this.openWrtInfo.systemInfo.model || this.openWrtInfo.systemInfo.board_name}`);
|
|
231
|
+
this.emit('devInfo', `System: ${this.openWrtInfo.systemInfo.system}`);
|
|
232
|
+
this.emit('devInfo', `Kernel: ${this.openWrtInfo.systemInfo.kernel}`);
|
|
233
|
+
this.emit('devInfo', `Firmware: ${this.openWrtInfo.systemInfo.release?.description}`);
|
|
234
|
+
this.emit('devInfo', `Target: ${this.openWrtInfo.systemInfo.release?.target}`);
|
|
235
|
+
this.emit('devInfo', `SSIDs: ${this.openWrtInfo.ssids.length}`);
|
|
236
|
+
this.emit('devInfo', `----------------------------------`);
|
|
237
|
+
}
|
|
238
238
|
|
|
239
239
|
//prepare accessory
|
|
240
240
|
const accessory = await this.prepareAccessory();
|
|
@@ -244,4 +244,4 @@ class AccessPointDevice extends EventEmitter {
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
};
|
|
247
|
-
export default
|
|
247
|
+
export default AccessPoint;
|
package/src/mqtt.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { connect } from 'mqtt';
|
|
2
|
+
import EventEmitter from 'events';
|
|
3
|
+
|
|
4
|
+
class Mqtt extends EventEmitter {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
super();
|
|
7
|
+
|
|
8
|
+
const url = `mqtt://${config.host}:${config.port}`;
|
|
9
|
+
const subscribeTopic = `${config.prefix}/Set`;
|
|
10
|
+
|
|
11
|
+
const options = {
|
|
12
|
+
clientId: config.clientId,
|
|
13
|
+
username: config.user,
|
|
14
|
+
password: config.passwd,
|
|
15
|
+
protocolVersion: 5,
|
|
16
|
+
clean: false,
|
|
17
|
+
properties: {
|
|
18
|
+
sessionExpiryInterval: 60 * 60, // 1 hour
|
|
19
|
+
userProperties: {
|
|
20
|
+
source: 'node-client'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
this.mqttClient = connect(url, options);
|
|
26
|
+
|
|
27
|
+
// === CONNECTED ===
|
|
28
|
+
this.mqttClient.on('connect', async (packet) => {
|
|
29
|
+
this.emit('connected', 'MQTT v5 connected.');
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const result = await this.mqttClient.subscribeAsync(subscribeTopic, {
|
|
33
|
+
qos: 1,
|
|
34
|
+
properties: {
|
|
35
|
+
userProperties: {
|
|
36
|
+
type: 'subscription'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// MQTT v5 subscription results contain reason codes
|
|
42
|
+
if (config.logDebug) this.emit('debug', `Subscribed to ${subscribeTopic}, reason codes: ${JSON.stringify(result)}`);
|
|
43
|
+
this.emit('subscribed', `MQTT Subscribe topic: ${subscribeTopic}`);
|
|
44
|
+
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (config.logWarn) this.emit('warn', `MQTT Subscribe error: ${error}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// === MESSAGE ===
|
|
51
|
+
this.mqttClient.on('message', (topic, payload, packet) => {
|
|
52
|
+
try {
|
|
53
|
+
const obj = JSON.parse(payload.toString());
|
|
54
|
+
if (config.logDebug) this.emit('debug', `MQTT Received:\nTopic: ${topic}\nPayload: ${JSON.stringify(obj, null, 2)}\nProperties: ${JSON.stringify(packet.properties, null, 2)}`);
|
|
55
|
+
|
|
56
|
+
const key = Object.keys(obj)[0];
|
|
57
|
+
const value = Object.values(obj)[0];
|
|
58
|
+
this.emit('set', key, value);
|
|
59
|
+
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (config.logWarn) this.emit('warn', `MQTT Parse error: ${error}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// === PUBLISH EVENT ===
|
|
66
|
+
this.on('publish', async (topic, message) => {
|
|
67
|
+
try {
|
|
68
|
+
const fullTopic = `${config.prefix}/${topic}`;
|
|
69
|
+
const publishMessage = JSON.stringify(message);
|
|
70
|
+
|
|
71
|
+
await this.mqttClient.publishAsync(fullTopic, publishMessage, {
|
|
72
|
+
qos: 1,
|
|
73
|
+
properties: {
|
|
74
|
+
contentType: 'application/json',
|
|
75
|
+
userProperties: {
|
|
76
|
+
source: 'node',
|
|
77
|
+
action: 'set'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (config.logDebug) this.emit('debug', `MQTT Publish:\nTopic: ${fullTopic}\nPayload: ${publishMessage}`);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (config.logWarn) this.emit('warn', `MQTT Publish error: ${error}`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// === ERRORS / STATE ===
|
|
89
|
+
this.mqttClient.on('error', (err) => {
|
|
90
|
+
if (config.logWarn) this.emit('warn', `MQTT Error: ${err.message}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.mqttClient.on('reconnect', () => {
|
|
94
|
+
if (config.logDebug) this.emit('debug', 'MQTT Reconnecting...');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this.mqttClient.on('close', () => {
|
|
98
|
+
if (config.logDebug) this.emit('debug', 'MQTT Connection closed.');
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default Mqtt;
|
package/src/openwrt.js
CHANGED
|
@@ -1,140 +1,150 @@
|
|
|
1
|
-
import EventEmitter from
|
|
2
|
-
import
|
|
3
|
-
import Functions from
|
|
4
|
-
import ImpulseGenerator from
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import Functions from './functions.js';
|
|
4
|
+
import ImpulseGenerator from './impulsegenerator.js';
|
|
5
5
|
|
|
6
6
|
class OpenWrt extends EventEmitter {
|
|
7
7
|
constructor(config) {
|
|
8
8
|
super();
|
|
9
9
|
|
|
10
|
-
this.
|
|
11
|
-
this.
|
|
12
|
-
this.passwd = config.auth?.passwd || "";
|
|
13
|
-
|
|
10
|
+
this.user = config.auth?.user || 'root';
|
|
11
|
+
this.passwd = config.auth?.passwd;
|
|
14
12
|
this.logError = config.log?.error;
|
|
15
13
|
this.logDebug = config.log?.debug;
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
//external integration
|
|
16
|
+
this.restFulEnabled = config.restFul?.enable || false;
|
|
17
|
+
this.mqttEnabled = config.mqtt?.enable || false;
|
|
18
|
+
|
|
18
19
|
this.lock = false;
|
|
20
|
+
this.sessionId = null;
|
|
21
|
+
this.sessionExpiresAt = 0;
|
|
19
22
|
|
|
20
23
|
this.functions = new Functions();
|
|
21
24
|
|
|
25
|
+
const baseUrl = `http://${config.host}/ubus`;
|
|
26
|
+
this.axiosInstance = axios.create({
|
|
27
|
+
baseURL: baseUrl,
|
|
28
|
+
timeout: 5000,
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json"
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
22
34
|
this.impulseGenerator = new ImpulseGenerator()
|
|
23
|
-
.on(
|
|
35
|
+
.on('connect', () => this.handleWithLock(async () => {
|
|
24
36
|
await this.connect();
|
|
25
37
|
}))
|
|
26
|
-
.on(
|
|
27
|
-
this.emit(state ?
|
|
38
|
+
.on('state', (state) => {
|
|
39
|
+
this.emit(state ? 'success' : 'warn', `Impulse generator ${state ? 'started' : 'stopped'}`);
|
|
28
40
|
});
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
async handleWithLock(fn) {
|
|
32
44
|
if (this.lock) return;
|
|
33
45
|
this.lock = true;
|
|
46
|
+
|
|
34
47
|
try {
|
|
35
48
|
await fn();
|
|
36
49
|
} catch (error) {
|
|
37
|
-
this.emit(
|
|
50
|
+
this.emit('error', `Impulse generator error: ${error.message}`);
|
|
38
51
|
} finally {
|
|
39
52
|
this.lock = false;
|
|
40
53
|
}
|
|
41
54
|
}
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
+
async login() {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
if (this.sessionId && now < this.sessionExpiresAt) {
|
|
59
|
+
return this.sessionId;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const response = await this.axiosInstance.post('', {
|
|
63
|
+
jsonrpc: '2.0',
|
|
64
|
+
id: 1,
|
|
65
|
+
method: 'call',
|
|
66
|
+
params: ['00000000000000000000000000000000', 'session', 'login', { username: this.user, password: this.passwd }]
|
|
56
67
|
});
|
|
68
|
+
|
|
69
|
+
const result = response.data?.result?.[1];
|
|
70
|
+
if (!result?.ubus_rpc_session) throw new Error('Ubus login failed');
|
|
71
|
+
|
|
72
|
+
this.sessionId = result.ubus_rpc_session;
|
|
73
|
+
this.sessionExpiresAt = now + 240_000;
|
|
74
|
+
|
|
75
|
+
if (this.logDebug) this.emit('debug', `Ubus login OK`);
|
|
76
|
+
return this.sessionId;
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
// --- ubus call przez SSH ---
|
|
60
79
|
async ubusCall(service, method, params = {}) {
|
|
61
|
-
const
|
|
62
|
-
|
|
80
|
+
const session = await this.login();
|
|
81
|
+
const response = await this.axiosInstance.post('', {
|
|
82
|
+
jsonrpc: '2.0',
|
|
83
|
+
id: 2,
|
|
84
|
+
method: 'call',
|
|
85
|
+
params: [session, service, method, params]
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (response.data?.error) throw new Error(response.data.error.message || 'Ubus call error');
|
|
89
|
+
|
|
90
|
+
return response.data.result[1];
|
|
63
91
|
}
|
|
64
92
|
|
|
65
|
-
// --- Pobranie info i statusu ---
|
|
66
93
|
async connect() {
|
|
67
|
-
const openWrtInfo = { state: false, systemInfo: {}, wirelessStatus: {}, wirelessRadios: [], ssids: [] };
|
|
68
94
|
try {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
const openWrtInfo = { state: false, info: '', systemInfo: {}, networkInfo: {}, wirelessStatus: {}, wirelessRadios: [], ssids: [] }
|
|
96
|
+
const systemInfo = await this.ubusCall('system', 'board');
|
|
97
|
+
if (this.logDebug) this.emit('debug', `System info data: ${JSON.stringify(systemInfo, null, 2)}`);
|
|
98
|
+
|
|
99
|
+
//const networkInfo = await this.ubusCall('network.device', 'status', '{ "name": "eth0" }');
|
|
100
|
+
//const networkInfo = await this.ubusCall('file', 'read', { path: '/sys/class/net/eth0/address' });
|
|
101
|
+
//if (this.logDebug) this.emit('debug', `Network info data: ${networkInfo}`);
|
|
102
|
+
|
|
103
|
+
//const wirelessStatus = await this.ubusCall('network.wireless', 'status');
|
|
104
|
+
const wirelessStatus = await this.ubusCall('uci', 'get', { config: 'wireless' });
|
|
105
|
+
if (this.logDebug) this.emit('debug', `Wireless status data: ${JSON.stringify(wirelessStatus, null, 2)}`);
|
|
106
|
+
|
|
107
|
+
//const wirelessRadios = Object.values(wirelessStatus.radios).map(radio => ({
|
|
108
|
+
//name: radio.name,
|
|
109
|
+
//state: radio.up === true,
|
|
110
|
+
//interfaces: Object.values(radio.interfaces).map(i => ({
|
|
111
|
+
//name: i.ssid,
|
|
112
|
+
//state: i.up === true,
|
|
113
|
+
//mode: i.mode
|
|
114
|
+
//}))
|
|
115
|
+
//}));
|
|
116
|
+
|
|
117
|
+
//const ssids = wirelessRadios.flatMap(radio => radio.interfaces);
|
|
118
|
+
const ssids = Object.entries(wirelessStatus.values || {}).flatMap(([key, data]) => {
|
|
119
|
+
if (!key.startsWith('wifinet') && !key.startsWith('default_radio')) return [];
|
|
120
|
+
return [{
|
|
121
|
+
ifname: data['.name'] || key,
|
|
122
|
+
name: data.ssid || null,
|
|
123
|
+
device: data.device || null,
|
|
124
|
+
mode: data.mode || null,
|
|
125
|
+
hidden: data.hidden === '1' || data.hidden === true,
|
|
126
|
+
state: true
|
|
127
|
+
}];
|
|
128
|
+
});
|
|
129
|
+
if (ssids.length === 0) {
|
|
130
|
+
openWrtInfo.info = 'SSIDs not found';
|
|
131
|
+
return openWrtInfo;
|
|
95
132
|
}
|
|
96
133
|
|
|
97
134
|
openWrtInfo.state = true;
|
|
135
|
+
openWrtInfo.info = `Connect Success`;
|
|
98
136
|
openWrtInfo.systemInfo = systemInfo;
|
|
137
|
+
//openWrtInfo.networkInfo = networkInfo;
|
|
99
138
|
openWrtInfo.wirelessStatus = wirelessStatus;
|
|
100
|
-
openWrtInfo.wirelessRadios = wirelessRadios;
|
|
139
|
+
//openWrtInfo.wirelessRadios = wirelessRadios;
|
|
101
140
|
openWrtInfo.ssids = ssids;
|
|
102
141
|
|
|
103
|
-
|
|
142
|
+
// emit data
|
|
143
|
+
this.emit('openWrtInfo', openWrtInfo);
|
|
144
|
+
|
|
104
145
|
return openWrtInfo;
|
|
105
146
|
} catch (error) {
|
|
106
|
-
|
|
107
|
-
return openWrtInfo;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// --- Włączanie / wyłączanie SSID ---
|
|
112
|
-
async send(type, ssidName, state) {
|
|
113
|
-
switch (type) {
|
|
114
|
-
case "ssid":
|
|
115
|
-
await this.handleWithLock(async () => {
|
|
116
|
-
if (this.logDebug) this.emit("debug", `[${this.host}] ${state ? "Enabling" : "Disabling"} SSID ${ssidName}`);
|
|
117
|
-
|
|
118
|
-
const status = await this.ubusCall("network.wireless", "status");
|
|
119
|
-
const iface = await this.functions.findIfaceBySsid(status, ssidName);
|
|
120
|
-
if (!iface) throw new Error(`SSID ${ssidName} not found`);
|
|
121
|
-
|
|
122
|
-
const section = iface.section;
|
|
123
|
-
if (!section) throw new Error(`No UCI section for SSID ${ssidName}`);
|
|
124
|
-
|
|
125
|
-
await this.ubusCall("uci", "set", {
|
|
126
|
-
config: "wireless",
|
|
127
|
-
section: section,
|
|
128
|
-
values: { disabled: state ? "0" : "1" }
|
|
129
|
-
});
|
|
130
|
-
await this.ubusCall("uci", "commit", { config: "wireless" });
|
|
131
|
-
await this.ubusCall("network.wireless", "reload", {});
|
|
132
|
-
|
|
133
|
-
if (this.logDebug) this.emit("debug", `[${this.host}] SSID ${ssidName} ${state ? "enabled" : "disabled"}`);
|
|
134
|
-
});
|
|
135
|
-
break;
|
|
136
|
-
case "switch":
|
|
137
|
-
break;
|
|
147
|
+
throw new Error(`Connect error: ${error.message}`);
|
|
138
148
|
}
|
|
139
149
|
}
|
|
140
150
|
|
|
@@ -173,5 +183,4 @@ class OpenWrt extends EventEmitter {
|
|
|
173
183
|
}
|
|
174
184
|
}
|
|
175
185
|
|
|
176
|
-
export default OpenWrt;
|
|
177
|
-
|
|
186
|
+
export default OpenWrt;
|
package/src/restful.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import express, { json } from 'express';
|
|
2
|
+
import EventEmitter from 'events';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MESSAGE = 'This data is not available at this time.';
|
|
5
|
+
|
|
6
|
+
class RestFul extends EventEmitter {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super();
|
|
9
|
+
this.port = config.port;
|
|
10
|
+
this.logWarn = config.logWarn;
|
|
11
|
+
this.logDebug = config.logDebug;
|
|
12
|
+
|
|
13
|
+
this.restFulData = {
|
|
14
|
+
info: DEFAULT_MESSAGE,
|
|
15
|
+
state: DEFAULT_MESSAGE
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
this.connect();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
connect() {
|
|
22
|
+
try {
|
|
23
|
+
const app = express();
|
|
24
|
+
app.set('json spaces', 2);
|
|
25
|
+
app.use(json());
|
|
26
|
+
|
|
27
|
+
// Register GET routes for all keys
|
|
28
|
+
for (const key of Object.keys(this.restFulData)) {
|
|
29
|
+
app.get(`/${key}`, (req, res) => {
|
|
30
|
+
res.json(this.restFulData[key]);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Health check route
|
|
35
|
+
app.get('/status', (req, res) => {
|
|
36
|
+
res.json({
|
|
37
|
+
status: 'online',
|
|
38
|
+
uptime: process.uptime(),
|
|
39
|
+
available_paths: Object.keys(this.restFulData).map(k => `/${k}`)
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// POST route to update values
|
|
44
|
+
app.post('/', (req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
const obj = req.body;
|
|
47
|
+
if (!obj || typeof obj !== 'object' || Object.keys(obj).length === 0) {
|
|
48
|
+
if (this.logWarn) this.emit('warn', 'RESTFul Invalid JSON payload');
|
|
49
|
+
return res.status(400).json({ error: 'RESTFul Invalid JSON payload' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const key = Object.keys(obj)[0];
|
|
53
|
+
const value = obj[key];
|
|
54
|
+
this.emit('set', key, value);
|
|
55
|
+
this.update(key, value);
|
|
56
|
+
|
|
57
|
+
if (this.logDebug) this.emit('debug', `RESTFul post data: ${JSON.stringify(obj, null, 2)}`);
|
|
58
|
+
|
|
59
|
+
res.json({ success: true, received: obj });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (this.logWarn) this.emit('warn', `RESTFul Parse error: ${error}`);
|
|
62
|
+
res.status(500).json({ error: 'RESTFul Internal Server Error' });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Start the server
|
|
67
|
+
app.listen(this.port, () => {
|
|
68
|
+
this.emit('connected', `RESTful started on port: ${this.port}`);
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (this.logWarn) this.emit('warn', `RESTful Connect error: ${error}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
update(path, data) {
|
|
76
|
+
if (this.restFulData.hasOwnProperty(path)) {
|
|
77
|
+
this.restFulData[path] = data;
|
|
78
|
+
} else {
|
|
79
|
+
if (this.logWarn) this.emit('warn', `Unknown RESTFul update path: ${path}, data: ${JSON.stringify(data)}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (this.logDebug) this.emit('debug', `RESTFul update path: ${path}, data: ${JSON.stringify(data)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default RestFul;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import EventEmitter from 'events';
|
|
2
|
+
import RestFul from './restful.js';
|
|
3
|
+
import Mqtt from './mqtt.js';
|
|
2
4
|
let Accessory, Characteristic, Service, Categories, AccessoryUUID;
|
|
3
5
|
|
|
4
|
-
class
|
|
6
|
+
class Switch extends EventEmitter {
|
|
5
7
|
constructor(api, config, openWrt, openWrtInfo) {
|
|
6
8
|
super();
|
|
7
9
|
|
|
@@ -12,8 +14,12 @@ class SwitchDevice extends EventEmitter {
|
|
|
12
14
|
AccessoryUUID = api.hap.uuid;
|
|
13
15
|
|
|
14
16
|
//config
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.host = config.host;
|
|
19
|
+
this.name = config.apDevice?.name || openWrtInfo.systemInfo.hostname;
|
|
20
|
+
this.namePrefix = config.apDevice?.namePrefix || false;
|
|
21
|
+
this.sensorsEnabled = config.apDevice?.sensor || false
|
|
22
|
+
this.logDeviceInfo = config.log?.deviceInfo || false;
|
|
17
23
|
this.logInfo = config.log?.info || false;
|
|
18
24
|
this.logDebug = config.log?.debug || false;
|
|
19
25
|
|
|
@@ -26,7 +32,37 @@ class SwitchDevice extends EventEmitter {
|
|
|
26
32
|
//openwrt
|
|
27
33
|
this.openWrt = openWrt;
|
|
28
34
|
this.openWrtInfo = openWrtInfo;
|
|
29
|
-
|
|
35
|
+
|
|
36
|
+
//openwrt client
|
|
37
|
+
openWrt.on('openWrtInfo', (openWrtInfo) => {
|
|
38
|
+
this.informationService?.updateCharacteristic(Characteristic.FirmwareRevision, openWrtInfo.systemInfo.release?.version);
|
|
39
|
+
|
|
40
|
+
// update state
|
|
41
|
+
const ports = openWrtInfo.ports;
|
|
42
|
+
for (let i = 0; i < ports.length; i++) {
|
|
43
|
+
const port = ports[i];
|
|
44
|
+
const name = port.name;
|
|
45
|
+
const state = port.state;
|
|
46
|
+
const serviceName = this.namePrefix ? `${this.name} ${name}` : name;
|
|
47
|
+
this.services?.[i]
|
|
48
|
+
?.setCharacteristic(Characteristic.ConfiguredName, serviceName)
|
|
49
|
+
.updateCharacteristic(Characteristic.On, state);
|
|
50
|
+
|
|
51
|
+
this.sensorServices?.[i]
|
|
52
|
+
?.setCharacteristic(Characteristic.ConfiguredName, serviceName)
|
|
53
|
+
.updateCharacteristic(Characteristic.ContactSensorState, state);
|
|
54
|
+
|
|
55
|
+
if (this.logInfo) {
|
|
56
|
+
this.emit('info', `Name: ${port.name}`);
|
|
57
|
+
this.emit('info', `State: ${port.state}`);
|
|
58
|
+
this.emit('info', `Mode: ${port.mode}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//restFul and mqtt
|
|
63
|
+
if (this.restFulConnected) this.emit('restFul', 'info', openWrtInfo);
|
|
64
|
+
if (this.mqttConnected) this.emit('mqtt', 'Info', openWrtInfo);
|
|
65
|
+
});
|
|
30
66
|
};
|
|
31
67
|
|
|
32
68
|
async externalIntegrations() {
|
|
@@ -65,8 +101,8 @@ class SwitchDevice extends EventEmitter {
|
|
|
65
101
|
this.mqtt1 = new Mqtt({
|
|
66
102
|
host: this.mqtt.host,
|
|
67
103
|
port: this.mqtt.port || 1883,
|
|
68
|
-
clientId: this.mqtt.clientId ? `${this.
|
|
69
|
-
prefix: this.mqtt.prefix ? `${this.
|
|
104
|
+
clientId: this.mqtt.clientId ? `${this.openWrtInfo.systemInfo.release.distribution || 'OpenWrt'}_${this.mqtt.clientId}_${Math.random().toString(16).slice(3)}` : `${this.openWrtInfo.systemInfo.release.distribution || 'OpenWrt'}_${Math.random().toString(16).slice(3)}`,
|
|
105
|
+
prefix: this.mqtt.prefix ? `${this.openWrtInfo.systemInfo.release.distribution || 'OpenWrt'}/${this.mqtt.prefix}/${this.name}` : `${this.openWrtInfo.systemInfo.release.distribution || 'OpenWrt'}/${this.name}`,
|
|
70
106
|
user: this.mqtt.auth?.user,
|
|
71
107
|
passwd: this.mqtt.auth?.passwd,
|
|
72
108
|
logWarn: this.logWarn,
|
|
@@ -121,29 +157,29 @@ class SwitchDevice extends EventEmitter {
|
|
|
121
157
|
//prepare accessory
|
|
122
158
|
if (this.logDebug) this.emit('debug', `prepare accessory`);
|
|
123
159
|
const accessoryName = this.name;
|
|
124
|
-
const accessoryUUID = AccessoryUUID.generate(this.openWrtInfo.systemInfo.
|
|
125
|
-
const accessoryCategory = Categories.
|
|
160
|
+
const accessoryUUID = AccessoryUUID.generate(this.host + this.openWrtInfo.systemInfo.system);
|
|
161
|
+
const accessoryCategory = Categories.AIRPORT;
|
|
126
162
|
const accessory = new Accessory(accessoryName, accessoryUUID, accessoryCategory);
|
|
127
163
|
|
|
128
164
|
//prepare information service
|
|
129
165
|
if (this.logDebug) this.emit('debug', `prepare information service`);
|
|
130
|
-
accessory.getService(Service.AccessoryInformation)
|
|
131
|
-
.setCharacteristic(Characteristic.Manufacturer, 'OpenWrt')
|
|
166
|
+
this.informationService = accessory.getService(Service.AccessoryInformation)
|
|
167
|
+
.setCharacteristic(Characteristic.Manufacturer, this.openWrtInfo.systemInfo.release.distribution || 'OpenWrt')
|
|
132
168
|
.setCharacteristic(Characteristic.Model, this.openWrtInfo.systemInfo.model)
|
|
133
169
|
.setCharacteristic(Characteristic.SerialNumber, this.openWrtInfo.systemInfo.system)
|
|
134
|
-
.setCharacteristic(Characteristic.FirmwareRevision, this.openWrtInfo.systemInfo.release?.
|
|
135
|
-
.setCharacteristic(Characteristic.ConfiguredName, accessoryName);
|
|
170
|
+
.setCharacteristic(Characteristic.FirmwareRevision, this.openWrtInfo.systemInfo.release?.version);
|
|
136
171
|
|
|
137
172
|
if (this.logDebug) this.emit('debug', `prepare service`);
|
|
138
173
|
|
|
139
|
-
//
|
|
174
|
+
//services
|
|
140
175
|
this.services = [];
|
|
141
|
-
|
|
176
|
+
this.sensorServices = [];
|
|
177
|
+
for (const port of this.openWrtInfo.ports) {
|
|
142
178
|
const name = port.name;
|
|
143
179
|
if (this.logDebug) this.emit('debug', `prepare port: ${name} service`);
|
|
144
180
|
|
|
145
|
-
const serviceName = this.
|
|
146
|
-
const service = accessory.addService(Service.Switch, serviceName, `service${
|
|
181
|
+
const serviceName = this.namePrefix ? `${accessoryName} ${name}` : name;
|
|
182
|
+
const service = accessory.addService(Service.Switch, serviceName, `service${name}`);
|
|
147
183
|
service.addOptionalCharacteristic(Characteristic.ConfiguredName);
|
|
148
184
|
service.setCharacteristic(Characteristic.ConfiguredName, serviceName);
|
|
149
185
|
service.getCharacteristic(Characteristic.On)
|
|
@@ -163,10 +199,8 @@ class SwitchDevice extends EventEmitter {
|
|
|
163
199
|
});
|
|
164
200
|
this.services.push(service);
|
|
165
201
|
|
|
166
|
-
if (this.
|
|
202
|
+
if (this.sensorsEnabled) {
|
|
167
203
|
if (this.logDebug) this.emit('debug', `prepare port: ${name} sensor service`);
|
|
168
|
-
|
|
169
|
-
this.sensorServices = [];
|
|
170
204
|
const sensorService = accessory.addService(Service.ContactSensor, serviceName, `sensorService${name}`);
|
|
171
205
|
sensorService.addOptionalCharacteristic(Characteristic.ConfiguredName);
|
|
172
206
|
sensorService.setCharacteristic(Characteristic.ConfiguredName, serviceName);
|
|
@@ -191,48 +225,16 @@ class SwitchDevice extends EventEmitter {
|
|
|
191
225
|
//start external integrations
|
|
192
226
|
if (this.restFul.enable || this.mqtt.enable) await this.externalIntegrations();
|
|
193
227
|
|
|
194
|
-
this.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
})
|
|
205
|
-
.on('switchStatus', async (status) => {
|
|
206
|
-
})
|
|
207
|
-
.on('switchPorts', async (ports) => {
|
|
208
|
-
// sensors
|
|
209
|
-
for (let i = 0; i < ports.length; i++) {
|
|
210
|
-
const port = ports[i];
|
|
211
|
-
|
|
212
|
-
const name = port[i].name;
|
|
213
|
-
const state = port[i].state;
|
|
214
|
-
const serviceName = this.accessPoint.namePrefix ? `${this.name} ${name}` : name;
|
|
215
|
-
this.services?.[i]
|
|
216
|
-
?.setCharacteristic(Characteristic.ConfiguredName, serviceName)
|
|
217
|
-
.updateCharacteristic(Characteristic.On, state);
|
|
218
|
-
|
|
219
|
-
this.sensorServices?.[i]
|
|
220
|
-
?.setCharacteristic(Characteristic.ConfiguredName, name)
|
|
221
|
-
.updateCharacteristic(Characteristic.ContactSensorState, state ? 0 : 1);
|
|
222
|
-
|
|
223
|
-
if (this.logInfo) {
|
|
224
|
-
this.emit('info', `Port name: ${port.name}`);
|
|
225
|
-
this.emit('info', `Name: ${port.state}`);
|
|
226
|
-
this.emit('info', `Mode: ${port.mode}`);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
})
|
|
230
|
-
.on('restFul', (path, data) => {
|
|
231
|
-
if (this.restFulConnected) this.restFul1.update(path, data);
|
|
232
|
-
})
|
|
233
|
-
.on('mqtt', (topic, message) => {
|
|
234
|
-
if (this.mqttConnected) this.mqtt1.emit('publish', topic, message);
|
|
235
|
-
});
|
|
228
|
+
if (this.logDeviceInfo) {
|
|
229
|
+
this.emit('devInfo', `-------- Access Point ${this.name} --------`);
|
|
230
|
+
this.emit('devInfo', `Model: ${this.openWrtInfo.systemInfo.model || this.openWrtInfo.systemInfo.board_name}`);
|
|
231
|
+
this.emit('devInfo', `System: ${this.openWrtInfo.systemInfo.system}`);
|
|
232
|
+
this.emit('devInfo', `Kernel: ${this.openWrtInfo.systemInfo.kernel}`);
|
|
233
|
+
this.emit('devInfo', `Firmware: ${this.openWrtInfo.systemInfo.release?.description}`);
|
|
234
|
+
this.emit('devInfo', `Target: ${this.openWrtInfo.systemInfo.release?.target}`);
|
|
235
|
+
this.emit('devInfo', `Ports: ${this.openWrtInfo.ports.length}`);
|
|
236
|
+
this.emit('devInfo', `----------------------------------`);
|
|
237
|
+
}
|
|
236
238
|
|
|
237
239
|
//prepare accessory
|
|
238
240
|
const accessory = await this.prepareAccessory();
|
|
@@ -242,4 +244,4 @@ class SwitchDevice extends EventEmitter {
|
|
|
242
244
|
}
|
|
243
245
|
}
|
|
244
246
|
};
|
|
245
|
-
export default
|
|
247
|
+
export default Switch;
|