@rfranzoi/scrypted-mqtt-securitysystem 0.1.1 → 0.1.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/out/plugin.zip +0 -0
- package/package.json +13 -4
- package/src/main.ts +0 -278
- package/tsconfig.json +0 -15
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rfranzoi/scrypted-mqtt-securitysystem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Scrypted plugin: Paradox Security System via MQTT (PAI/PAI-MQTT style).",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "out/main.nodejs.js",
|
|
7
7
|
"type": "commonjs",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "scrypted-webpack",
|
|
@@ -18,9 +18,18 @@
|
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^20.0.0"
|
|
20
20
|
},
|
|
21
|
+
"files": [
|
|
22
|
+
"out",
|
|
23
|
+
"README.md",
|
|
24
|
+
"package.json"
|
|
25
|
+
],
|
|
21
26
|
"scrypted": {
|
|
22
27
|
"name": "Paradox MQTT SecuritySystem",
|
|
23
|
-
"interfaces": [
|
|
28
|
+
"interfaces": [
|
|
29
|
+
"SecuritySystem",
|
|
30
|
+
"Settings",
|
|
31
|
+
"TamperSensor",
|
|
32
|
+
"Online"
|
|
33
|
+
]
|
|
24
34
|
}
|
|
25
35
|
}
|
|
26
|
-
|
package/src/main.ts
DELETED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import sdk, {
|
|
2
|
-
ScryptedDeviceBase,
|
|
3
|
-
ScryptedDeviceType,
|
|
4
|
-
Settings,
|
|
5
|
-
Setting,
|
|
6
|
-
SecuritySystem,
|
|
7
|
-
SecuritySystemMode,
|
|
8
|
-
TamperSensor,
|
|
9
|
-
Online,
|
|
10
|
-
} from '@scrypted/sdk';
|
|
11
|
-
|
|
12
|
-
import mqtt, { MqttClient, IClientOptions } from 'mqtt';
|
|
13
|
-
|
|
14
|
-
const { systemManager } = sdk;
|
|
15
|
-
|
|
16
|
-
function truthy(v?: string) {
|
|
17
|
-
if (!v) return false;
|
|
18
|
-
const s = v.toString().trim().toLowerCase();
|
|
19
|
-
return s === '1' || s === 'true' || s === 'online' || s === 'yes' || s === 'on' || s === 'ok';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function falsy(v?: string) {
|
|
23
|
-
if (!v) return false;
|
|
24
|
-
const s = v.toString().trim().toLowerCase();
|
|
25
|
-
return s === '0' || s === 'false' || s === 'offline' || s === 'no' || s === 'off';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function normalize(s: string) {
|
|
29
|
-
return (s || '').trim().toLowerCase();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Default payloads for PAI/PAI-MQTT-like setups */
|
|
33
|
-
const DEFAULT_OUTGOING: Record<SecuritySystemMode, string> = {
|
|
34
|
-
[SecuritySystemMode.Disarmed]: 'disarm',
|
|
35
|
-
[SecuritySystemMode.HomeArmed]: 'arm_home',
|
|
36
|
-
[SecuritySystemMode.AwayArmed]: 'arm_away',
|
|
37
|
-
[SecuritySystemMode.NightArmed]: 'arm_night',
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/** Common incoming synonyms → SecuritySystemMode */
|
|
41
|
-
function payloadToMode(payload: string | Buffer | undefined): SecuritySystemMode | undefined {
|
|
42
|
-
if (payload == null) return;
|
|
43
|
-
const p = normalize(payload.toString());
|
|
44
|
-
|
|
45
|
-
if (['disarm', 'disarmed', 'off', '0', 'idle', 'ready'].includes(p)) return SecuritySystemMode.Disarmed;
|
|
46
|
-
if (['arm_home', 'home', 'stay', 'armed_home'].includes(p)) return SecuritySystemMode.HomeArmed;
|
|
47
|
-
if (['arm_away', 'away', 'armed_away', 'arming', 'exit_delay'].includes(p)) return SecuritySystemMode.AwayArmed;
|
|
48
|
-
if (['arm_night', 'night', 'armed_night'].includes(p)) return SecuritySystemMode.NightArmed;
|
|
49
|
-
|
|
50
|
-
// Sometimes current_state may be 'alarm', 'entry_delay', etc.
|
|
51
|
-
if (['entry_delay', 'pending', 'arming'].includes(p)) return undefined;
|
|
52
|
-
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export default class ParadoxMqttSecuritySystem extends ScryptedDeviceBase
|
|
57
|
-
implements SecuritySystem, Settings, TamperSensor, Online {
|
|
58
|
-
|
|
59
|
-
private client?: MqttClient;
|
|
60
|
-
|
|
61
|
-
constructor() {
|
|
62
|
-
super();
|
|
63
|
-
|
|
64
|
-
// (facoltativo) Imposta il device type in UI
|
|
65
|
-
setTimeout(() => {
|
|
66
|
-
try {
|
|
67
|
-
(systemManager.getDeviceById(this.id) as any)?.setType?.(ScryptedDeviceType.SecuritySystem);
|
|
68
|
-
} catch {}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Default state
|
|
72
|
-
this.securitySystemState = this.securitySystemState || {
|
|
73
|
-
mode: SecuritySystemMode.Disarmed,
|
|
74
|
-
supportedModes: [
|
|
75
|
-
SecuritySystemMode.Disarmed,
|
|
76
|
-
SecuritySystemMode.HomeArmed,
|
|
77
|
-
SecuritySystemMode.AwayArmed,
|
|
78
|
-
SecuritySystemMode.NightArmed,
|
|
79
|
-
],
|
|
80
|
-
};
|
|
81
|
-
this.online = this.online ?? false;
|
|
82
|
-
|
|
83
|
-
// Connect on start
|
|
84
|
-
this.connectMqtt().catch(e => this.console.error('MQTT connect error:', e));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// --- Settings UI ---
|
|
88
|
-
|
|
89
|
-
async getSettings(): Promise<Setting[]> {
|
|
90
|
-
return [
|
|
91
|
-
{ group: 'MQTT', key: 'brokerUrl', title: 'Broker URL', placeholder: 'mqtt://127.0.0.1:1883', value: this.storage.getItem('brokerUrl') || 'mqtt://127.0.0.1:1883' },
|
|
92
|
-
{ group: 'MQTT', key: 'username', title: 'Username', type: 'string', value: this.storage.getItem('username') || '' },
|
|
93
|
-
{ group: 'MQTT', key: 'password', title: 'Password', type: 'password', value: this.storage.getItem('password') || '' },
|
|
94
|
-
{ group: 'MQTT', key: 'clientId', title: 'Client ID', placeholder: 'scrypted-paradox', value: this.storage.getItem('clientId') || 'scrypted-paradox' },
|
|
95
|
-
{ group: 'MQTT', key: 'tls', title: 'Use TLS', type: 'boolean', value: this.storage.getItem('tls') === 'true' },
|
|
96
|
-
{ group: 'MQTT', key: 'rejectUnauthorized', title: 'Reject Unauthorized (TLS)', type: 'boolean', value: this.storage.getItem('rejectUnauthorized') !== 'false', description: 'Disattiva solo con broker self-signed.' },
|
|
97
|
-
|
|
98
|
-
{ group: 'Topics', key: 'topicSetTarget', title: 'Set Target State (publish)', placeholder: 'paradox/control/partitions/Area_1', value: this.storage.getItem('topicSetTarget') || '' },
|
|
99
|
-
{ group: 'Topics', key: 'topicGetTarget', title: 'Get Target State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/target_state', value: this.storage.getItem('topicGetTarget') || '' },
|
|
100
|
-
{ group: 'Topics', key: 'topicGetCurrent', title: 'Get Current State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/current_state', value: this.storage.getItem('topicGetCurrent') || '' },
|
|
101
|
-
{ group: 'Topics', key: 'topicTamper', title: 'Get Status Tampered (subscribe)', placeholder: 'paradox/states/system/troubles/zone_tamper_trouble', value: this.storage.getItem('topicTamper') || '' },
|
|
102
|
-
{ group: 'Topics', key: 'topicOnline', title: 'Get Online (subscribe)', placeholder: 'paradox/interface/availability', value: this.storage.getItem('topicOnline') || '' },
|
|
103
|
-
|
|
104
|
-
{ group: 'Publish Options', key: 'qos', title: 'QoS', type: 'integer', value: parseInt(this.storage.getItem('qos') || '0') },
|
|
105
|
-
{ group: 'Publish Options', key: 'retain', title: 'Retain', type: 'boolean', value: this.storage.getItem('retain') === 'true' },
|
|
106
|
-
|
|
107
|
-
{ group: 'Outgoing Payloads', key: 'payloadDisarm', title: 'Payload Disarm', value: this.storage.getItem('payloadDisarm') || DEFAULT_OUTGOING[SecuritySystemMode.Disarmed] },
|
|
108
|
-
{ group: 'Outgoing Payloads', key: 'payloadHome', title: 'Payload HomeArmed', value: this.storage.getItem('payloadHome') || DEFAULT_OUTGOING[SecuritySystemMode.HomeArmed] },
|
|
109
|
-
{ group: 'Outgoing Payloads', key: 'payloadAway', title: 'Payload AwayArmed', value: this.storage.getItem('payloadAway') || DEFAULT_OUTGOING[SecuritySystemMode.AwayArmed] },
|
|
110
|
-
{ group: 'Outgoing Payloads', key: 'payloadNight', title: 'Payload NightArmed', value: this.storage.getItem('payloadNight') || DEFAULT_OUTGOING[SecuritySystemMode.NightArmed] },
|
|
111
|
-
];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async putSetting(key: string, value: string | number | boolean): Promise<void> {
|
|
115
|
-
this.storage.setItem(key, String(value));
|
|
116
|
-
await this.connectMqtt(true);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// --- MQTT ---
|
|
120
|
-
|
|
121
|
-
private getMqttOptions(): { url: string, opts: IClientOptions } {
|
|
122
|
-
const url = this.storage.getItem('brokerUrl') || 'mqtt://127.0.0.1:1883';
|
|
123
|
-
const username = this.storage.getItem('username') || undefined;
|
|
124
|
-
const password = this.storage.getItem('password') || undefined;
|
|
125
|
-
const clientId = this.storage.getItem('clientId') || 'scrypted-paradox';
|
|
126
|
-
const tls = this.storage.getItem('tls') === 'true';
|
|
127
|
-
const rejectUnauthorized = this.storage.getItem('rejectUnauthorized') !== 'false';
|
|
128
|
-
|
|
129
|
-
const opts: IClientOptions = {
|
|
130
|
-
clientId,
|
|
131
|
-
username,
|
|
132
|
-
password,
|
|
133
|
-
clean: true,
|
|
134
|
-
reconnectPeriod: 3000,
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
if (tls) {
|
|
138
|
-
(opts as any).protocol = 'mqtts';
|
|
139
|
-
(opts as any).rejectUnauthorized = rejectUnauthorized;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return { url, opts };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private async connectMqtt(reconnect = false) {
|
|
146
|
-
const subs = [
|
|
147
|
-
this.storage.getItem('topicGetTarget'),
|
|
148
|
-
this.storage.getItem('topicGetCurrent'),
|
|
149
|
-
this.storage.getItem('topicTamper'),
|
|
150
|
-
this.storage.getItem('topicOnline'),
|
|
151
|
-
].filter(Boolean) as string[];
|
|
152
|
-
|
|
153
|
-
if (!subs.length && !this.storage.getItem('topicSetTarget')) {
|
|
154
|
-
this.console.warn('Configura almeno un topic nelle impostazioni.');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (this.client) {
|
|
158
|
-
try { this.client.end(true); } catch {}
|
|
159
|
-
this.client = undefined;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const { url, opts } = this.getMqttOptions();
|
|
163
|
-
this.console.log(`Connecting MQTT ${url} ...`);
|
|
164
|
-
const client = mqtt.connect(url, opts);
|
|
165
|
-
this.client = client;
|
|
166
|
-
|
|
167
|
-
client.on('connect', () => {
|
|
168
|
-
this.console.log('MQTT connected');
|
|
169
|
-
this.online = true;
|
|
170
|
-
if (subs.length) {
|
|
171
|
-
client.subscribe(subs, { qos: 0 }, (err?: Error | null) => {
|
|
172
|
-
if (err) this.console.error('subscribe error', err);
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
client.on('reconnect', () => this.console.log('MQTT reconnecting...'));
|
|
178
|
-
client.on('close', () => { this.console.log('MQTT closed'); this.online = false; });
|
|
179
|
-
client.on('error', (e: Error) => { this.console.error('MQTT error', e); });
|
|
180
|
-
|
|
181
|
-
client.on('message', (topic: string, payload: Buffer) => {
|
|
182
|
-
try {
|
|
183
|
-
const p = payload?.toString() ?? '';
|
|
184
|
-
// Online
|
|
185
|
-
if (topic === this.storage.getItem('topicOnline')) {
|
|
186
|
-
if (truthy(p) || p.toLowerCase() === 'online') this.online = true;
|
|
187
|
-
if (falsy(p) || p.toLowerCase() === 'offline') this.online = false;
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Tamper
|
|
192
|
-
if (topic === this.storage.getItem('topicTamper')) {
|
|
193
|
-
const np = normalize(p);
|
|
194
|
-
if (truthy(np) || ['tamper', 'intrusion', 'cover', 'motion', 'magnetic'].includes(np)) {
|
|
195
|
-
(this as any).tampered = (['cover','intrusion','motion','magnetic'].find(x => x === np) as any) || true;
|
|
196
|
-
} else if (falsy(np)) {
|
|
197
|
-
(this as any).tampered = false;
|
|
198
|
-
}
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Target/Current → mode/triggered
|
|
203
|
-
if ([this.storage.getItem('topicGetTarget'), this.storage.getItem('topicGetCurrent')].includes(topic)) {
|
|
204
|
-
const mode = payloadToMode(payload);
|
|
205
|
-
const isAlarm = ['alarm', 'triggered'].includes(normalize(p));
|
|
206
|
-
const current = this.securitySystemState || { mode: SecuritySystemMode.Disarmed };
|
|
207
|
-
|
|
208
|
-
const newState = {
|
|
209
|
-
mode: mode ?? current.mode,
|
|
210
|
-
supportedModes: current.supportedModes ?? [
|
|
211
|
-
SecuritySystemMode.Disarmed,
|
|
212
|
-
SecuritySystemMode.HomeArmed,
|
|
213
|
-
SecuritySystemMode.AwayArmed,
|
|
214
|
-
SecuritySystemMode.NightArmed,
|
|
215
|
-
],
|
|
216
|
-
triggered: isAlarm || undefined,
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
this.securitySystemState = newState;
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
} catch (e) {
|
|
223
|
-
this.console.error('MQTT message handler error', e);
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// --- SecuritySystem commands ---
|
|
229
|
-
|
|
230
|
-
private publishSetTarget(payload: string) {
|
|
231
|
-
const topic = this.storage.getItem('topicSetTarget');
|
|
232
|
-
if (!topic || !this.client) {
|
|
233
|
-
this.console.warn('topicSetTarget o MQTT non configurati.');
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const retain = this.storage.getItem('retain') === 'true';
|
|
237
|
-
const qosNum = Number(this.storage.getItem('qos') || 0);
|
|
238
|
-
const qos = Math.max(0, Math.min(2, isFinite(qosNum) ? qosNum : 0)) as 0 | 1 | 2;
|
|
239
|
-
|
|
240
|
-
this.client.publish(
|
|
241
|
-
topic,
|
|
242
|
-
payload,
|
|
243
|
-
{ qos, retain },
|
|
244
|
-
(err?: Error | null) => {
|
|
245
|
-
if (err) this.console.error('publish error', err);
|
|
246
|
-
}
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async armSecuritySystem(mode: SecuritySystemMode): Promise<void> {
|
|
251
|
-
const payload = this.getOutgoing(mode);
|
|
252
|
-
this.console.log('armSecuritySystem', mode, '->', payload);
|
|
253
|
-
this.publishSetTarget(payload);
|
|
254
|
-
|
|
255
|
-
// Optimistic UI update until broker echoes target/current state
|
|
256
|
-
const st = this.securitySystemState || { mode: SecuritySystemMode.Disarmed };
|
|
257
|
-
this.securitySystemState = { ...st, mode, triggered: false };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async disarmSecuritySystem(): Promise<void> {
|
|
261
|
-
const payload = this.getOutgoing(SecuritySystemMode.Disarmed);
|
|
262
|
-
this.console.log('disarmSecuritySystem ->', payload);
|
|
263
|
-
this.publishSetTarget(payload);
|
|
264
|
-
|
|
265
|
-
const st = this.securitySystemState || { mode: SecuritySystemMode.Disarmed };
|
|
266
|
-
this.securitySystemState = { ...st, mode: SecuritySystemMode.Disarmed, triggered: false };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
private getOutgoing(mode: SecuritySystemMode) {
|
|
270
|
-
const map: Record<SecuritySystemMode, string> = {
|
|
271
|
-
[SecuritySystemMode.Disarmed]: this.storage.getItem('payloadDisarm') || DEFAULT_OUTGOING[SecuritySystemMode.Disarmed],
|
|
272
|
-
[SecuritySystemMode.HomeArmed]: this.storage.getItem('payloadHome') || DEFAULT_OUTGOING[SecuritySystemMode.HomeArmed],
|
|
273
|
-
[SecuritySystemMode.AwayArmed]: this.storage.getItem('payloadAway') || DEFAULT_OUTGOING[SecuritySystemMode.AwayArmed],
|
|
274
|
-
[SecuritySystemMode.NightArmed]: this.storage.getItem('payloadNight') || DEFAULT_OUTGOING[SecuritySystemMode.NightArmed],
|
|
275
|
-
};
|
|
276
|
-
return map[mode];
|
|
277
|
-
}
|
|
278
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"lib": ["ES2020", "DOM"],
|
|
5
|
-
"module": "CommonJS",
|
|
6
|
-
"moduleResolution": "Node",
|
|
7
|
-
"strict": true,
|
|
8
|
-
"esModuleInterop": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"outDir": "out",
|
|
11
|
-
"sourceMap": false
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/**/*.ts"]
|
|
14
|
-
}
|
|
15
|
-
|