@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 CHANGED
Binary file
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@rfranzoi/scrypted-mqtt-securitysystem",
3
- "version": "0.1.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": "dist/main.js",
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": ["SecuritySystem", "Settings", "TamperSensor", "Online"]
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
-