zenitel-client 0.1.0 → 0.1.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/README.md CHANGED
@@ -1,15 +1,75 @@
1
- # @pinecall/zenitel-client
1
+ # zenitel-client
2
+
3
+ [![npm](https://img.shields.io/npm/v/zenitel-client?color=818cf8&label=npm&style=flat-square)](https://www.npmjs.com/package/zenitel-client)
4
+ [![license](https://img.shields.io/npm/l/zenitel-client?color=818cf8&style=flat-square)](https://github.com/pinecall/zenitel-client/blob/master/LICENSE)
2
5
 
3
6
  > TypeScript client for Zenitel intercom systems — HTTP scraper, network scanner, and CLI.
4
7
 
5
- Zero runtime dependencies. Uses native `fetch` (Node 22+).
8
+ Zero runtime dependencies · Native `fetch` (Node 18+) · Tested on **TCIV-2+** firmware **9.2.3.0**
9
+
10
+ ## Table of Contents
11
+
12
+ - [Install](#install)
13
+ - [Quick Start](#quick-start)
14
+ - [CLI](#cli)
15
+ - [Device Discovery](#device-discovery)
16
+ - [Device Info](#device-information)
17
+ - [Call & Relay Control](#call-status--relays)
18
+ - [SIP Configuration](#sip-configuration)
19
+ - [Webcall Management](#webcall-api-management)
20
+ - [Config Backup & Restore](#config-backup--restore)
21
+ - [Call Button (DAK)](#call-button-dak-configuration)
22
+ - [Provisioning](#full-device-provisioning)
23
+ - [Video](#video)
24
+ - [Reboot](#device-reboot)
25
+ - [API](#api)
26
+ - [Connectivity](#connectivity)
27
+ - [Device Info](#device-info)
28
+ - [Calls](#webcall-place--stop--answer-calls)
29
+ - [Relay / Door Control](#relay--door-control)
30
+ - [SIP Config](#sip-configuration-1)
31
+ - [Webcall Toggle](#webcall-management)
32
+ - [Config Backup](#config-backup--restore-1)
33
+ - [Video URLs](#video-1)
34
+ - [Call Button (DAK)](#call-button-dak-configuration-1)
35
+ - [Provisioning](#full-device-provisioning-1)
36
+ - [Audio Settings](#audio-settings)
37
+ - [Reboot](#reboot)
38
+ - [Network Scanner](#scannetwork)
39
+ - [Supported Hardware](#supported-hardware)
40
+ - [Goform Endpoint Map](#goform-endpoint-map)
6
41
 
7
- Tested and validated against a real **TCIV-2+** (Turbine Compact - Video Plus), firmware **9.2.3.0**.
42
+ ---
8
43
 
9
44
  ## Install
10
45
 
11
46
  ```bash
12
- npm install @pinecall/zenitel-client
47
+ npm install zenitel-client
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```typescript
53
+ import { ZenitelClient } from 'zenitel-client';
54
+
55
+ const z = new ZenitelClient({ host: '192.168.1.143' });
56
+
57
+ // Check if device is reachable
58
+ await z.isReachable(); // true
59
+
60
+ // Get device info
61
+ const info = await z.getDeviceInfo();
62
+ console.log(info.model); // "TCIV-2+"
63
+
64
+ // Open the door for 7 seconds
65
+ await z.activateRelay({ timer: 7 });
66
+
67
+ // Read audio settings
68
+ const audio = await z.getAudioSettings();
69
+ console.log(audio.speaker.gain); // 0
70
+
71
+ // Set speaker volume to +3 dB
72
+ await z.setAudioSettings({ speaker: { gain: 3 } });
13
73
  ```
14
74
 
15
75
  ---
@@ -25,7 +85,6 @@ npx zenitel <command> [options]
25
85
  ### Device Discovery
26
86
 
27
87
  ```bash
28
- # Auto-scan network via ARP table (OUI 00:13:CB = Zenitel) + HTTP fingerprinting
29
88
  zenitel scan
30
89
 
31
90
  # 🔍 Scanning network for Zenitel devices...
@@ -46,8 +105,7 @@ zenitel scan
46
105
  ```bash
47
106
  zenitel info -h 192.168.1.143
48
107
 
49
- # 📋 Getting device info from 192.168.1.143...
50
- #
108
+ # 📋 Device info:
51
109
  # Model: TCIV-2+
52
110
  # System: Turbine Compact - Video Plus
53
111
  # Firmware: 9.2.3.0
@@ -56,125 +114,66 @@ zenitel info -h 192.168.1.143
56
114
  # Hostname: zenitel01
57
115
  # Mode: sip
58
116
  # Camera: Yes
59
- # Platform: turbine
60
- # HW Type: 8801
61
- # Uptime: up 2 days, 20 hours, 3 minutes
62
117
  # Webcall: Enabled
63
- # SIP Domain: testing-mo16m3gw.sip.twilio.com
64
118
  # SIP Registered: Yes
65
- # SIP Number: zenitel01
66
- # Outbound Proxy: testing-mo16m3gw.sip.twilio.com
67
119
  ```
68
120
 
69
121
  ### Call Status & Relays
70
122
 
71
123
  ```bash
72
- # Full status (call + all relays)
73
- zenitel status -h 192.168.1.143
74
-
75
- # Activate relay 1 for 3 seconds (opens door)
76
- zenitel relay -h 192.168.1.143
77
-
78
- # Activate GPIO output 2 for 5 seconds
79
- zenitel relay -h 192.168.1.143 --id gpio2 --timer 5
80
-
81
- # Deactivate relay
82
- zenitel relay -h 192.168.1.143 --off
83
-
84
- # Place a SIP call
85
- zenitel call 122 -h 192.168.1.143
86
-
87
- # Stop current call
88
- zenitel stop -h 192.168.1.143
124
+ zenitel status -h 192.168.1.143 # Full status
125
+ zenitel relay -h 192.168.1.143 # Open door (relay1, 3s)
126
+ zenitel relay -h 192.168.1.143 --id gpio2 --timer 5 # GPIO output
127
+ zenitel relay -h 192.168.1.143 --off # Deactivate
128
+ zenitel call 122 -h 192.168.1.143 # Place SIP call
129
+ zenitel stop -h 192.168.1.143 # Hang up
89
130
  ```
90
131
 
91
132
  ### SIP Configuration
92
133
 
93
134
  ```bash
94
- # Read current SIP config
95
- zenitel sip get -h 192.168.1.143
96
-
97
- # 📡 SIP config from 192.168.1.143:
98
- # Name: zenitel01
99
- # Number: zenitel01
100
- # Domain: testing-mo16m3gw.sip.twilio.com
101
- # Auth User: zenitel01
102
- # Auth Pass: ********
103
- # Proxy: testing-mo16m3gw.sip.twilio.com
104
- # Transport: UDP
105
-
106
- # Write SIP config (requires reboot to take effect)
135
+ zenitel sip get -h 192.168.1.143 # Read SIP config
107
136
  zenitel sip set -h 192.168.1.143 \
108
137
  --domain my-trunk.sip.twilio.com \
109
- --number station01 \
110
- --proxy my-trunk.sip.twilio.com \
111
- --transport udp
138
+ --number station01
112
139
  ```
113
140
 
114
141
  ### Webcall API Management
115
142
 
116
143
  ```bash
117
- # Check webcall status
118
- zenitel webcall -h 192.168.1.143
119
-
120
- # Enable webcall + relay HTTP API
121
- zenitel webcall enable -h 192.168.1.143
122
-
123
- # Disable webcall + relay HTTP API
124
- zenitel webcall disable -h 192.168.1.143
144
+ zenitel webcall -h 192.168.1.143 # Check status
145
+ zenitel webcall enable -h 192.168.1.143 # Enable HTTP API
146
+ zenitel webcall disable -h 192.168.1.143 # Disable
125
147
  ```
126
148
 
127
149
  ### Config Backup & Restore
128
150
 
129
151
  ```bash
130
- # Download full config as tar.gz
131
- zenitel backup -h 192.168.1.143 -o my_backup.tar.gz
132
-
133
- # Contents: ipst_config.xml, zapconfig.json, snmpd.conf, certs/, logos/
134
-
135
- # Restore config from backup
136
- zenitel restore my_backup.tar.gz -h 192.168.1.143
152
+ zenitel backup -h 192.168.1.143 -o backup.tar.gz
153
+ zenitel restore backup.tar.gz -h 192.168.1.143
137
154
  ```
138
155
 
139
156
  ### Video
140
157
 
141
158
  ```bash
142
159
  zenitel video -h 192.168.1.143
143
-
144
- # 📷 Video URLs for 192.168.1.143:
160
+ # 📷 Video URLs:
145
161
  # MJPG: http://192.168.1.143/mjpg/video.mjpg
146
162
  # RTSP: rtsp://192.168.1.143:554/1/RTSP
147
163
  ```
148
164
 
149
165
  ### Call Button (DAK) Configuration
150
166
 
151
- The DAK (Direct Access Key) controls what number the intercom dials when a visitor presses the call button. The value is stored inside `ipst_config.xml` in the device's config backup.
152
-
153
167
  ```bash
154
- # Read current call button config
155
168
  zenitel dak get -h 192.168.1.143
156
-
157
- # 📞 Call button (DAK1) config from 192.168.1.143:
158
- # Dial Number: 222
159
- # SIP Domain: testing-mo16m3gw.sip.twilio.com
160
- # Full URI: 222@testing-mo16m3gw.sip.twilio.com
161
-
162
- # Set call button to dial a different number
163
- # (downloads backup → modifies XML → re-uploads → reboots)
164
169
  zenitel dak set --number portia-ae3c -h 192.168.1.143
165
-
166
- # Set without auto-reboot
167
- zenitel dak set --number portia-ae3c --no-reboot -h 192.168.1.143
168
-
169
- # Set with explicit SIP domain
170
- zenitel dak set --number portia-ae3c --domain my.sip.twilio.com -h 192.168.1.143
171
170
  ```
172
171
 
173
- > **How it works:** The command downloads the device's full config backup (`ipst_config.tar.gz`), extracts `ipst_config.xml`, replaces the `<dak1><val>` field with the new SIP URI, re-packs the tar.gz (with correct checksums), uploads it, and reboots the device. Zero external dependencies — tar parsing is done in pure Node.js using `node:zlib`.
172
+ > Downloads config modifies XML re-uploads reboots. Zero external dependencies.
174
173
 
175
174
  ### Full Device Provisioning
176
175
 
177
- Configures a **factory-reset** Zenitel in a single command. Sets SIP credentials, call button target, enables webcall + auto-answer, and reboots.
176
+ One command to configure a factory-reset Zenitel:
178
177
 
179
178
  ```bash
180
179
  zenitel provision -h 192.168.1.143 \
@@ -183,42 +182,30 @@ zenitel provision -h 192.168.1.143 \
183
182
  --sip-pass 'your-sip-password' \
184
183
  --number portia-ae3c
185
184
 
186
- # 🔧 Provisioning 192.168.1.143 from factory reset...
187
- #
188
- # SIP Domain: testing-mo16m3gw.sip.twilio.com
189
- # SIP Auth User: zenitel01
190
- # Call Button →: portia-ae3c@testing-mo16m3gw.sip.twilio.com
191
- # Webcall: Enabled
192
- # Auto-answer: Enabled
193
- #
194
185
  # ✅ Provisioned! Device is rebooting (~30s).
195
186
  ```
196
187
 
197
- > **Under the hood:** Downloads config backup, modifies 9 XML fields in `ipst_config.xml` (SIP identity, domain, auth credentials, transport, outbound proxy, DAK1 value, webcall enable, auto-answer mode), re-packs with correct tar checksums, uploads, and reboots. One operation, one reboot, zero manual steps.
198
-
199
188
  ### Device Reboot
200
189
 
201
190
  ```bash
202
191
  zenitel reboot -h 192.168.1.143
203
- # Device will be offline for ~30 seconds
204
192
  ```
205
193
 
206
194
  ### CLI Options
207
195
 
208
196
  | Flag | Short | Description | Default |
209
197
  |------|-------|-------------|---------|
210
- | `--host` | `-h` | IP address of the Zenitel | required |
198
+ | `--host` | `-h` | Device IP address | required |
211
199
  | `--user` | `-u` | Web UI username | `admin` |
212
- | `--pass` | `-p` | Web UI password | (device default) |
200
+ | `--pass` | `-p` | Web UI password | `alphaadmin` |
213
201
  | `--id` | | Relay ID (`relay1`, `gpio1`–`gpio6`) | `relay1` |
214
- | `--timer` | | Relay timer in seconds | `3` |
215
- | `--timeout` | | Scan timeout in ms | `5000` |
202
+ | `--timer` | | Relay timer (seconds) | `3` |
203
+ | `--timeout` | | Scan timeout (ms) | `5000` |
216
204
  | `--off` | | Deactivate relay | |
217
205
  | `--domain` | | SIP domain | |
218
- | `--number` | | SIP directory number / agent number | |
206
+ | `--number` | | SIP number / agent number | |
219
207
  | `--proxy` | | Outbound proxy | |
220
208
  | `--transport` | | SIP transport (`udp`/`tcp`/`tls`) | |
221
- | `--name` | | Display name | |
222
209
  | `--out` | `-o` | Backup output filename | `ipst_config.tar.gz` |
223
210
  | `--no-reboot` | | Skip reboot after DAK set | |
224
211
  | `--sip-user` | | SIP auth username (provision) | |
@@ -230,15 +217,14 @@ zenitel reboot -h 192.168.1.143
230
217
 
231
218
  ### `ZenitelClient`
232
219
 
233
- The main class for interacting with a single Zenitel intercom via its web UI (HTTP).
234
-
235
220
  ```typescript
236
- import { ZenitelClient } from '@pinecall/zenitel-client';
221
+ import { ZenitelClient } from 'zenitel-client';
237
222
 
238
223
  const z = new ZenitelClient({
239
224
  host: '192.168.1.143',
240
- user: 'admin', // default
241
- password: 'your-password'
225
+ user: 'admin', // default
226
+ password: 'alphaadmin', // default
227
+ timeout: 5000, // ms, default
242
228
  });
243
229
  ```
244
230
 
@@ -252,274 +238,175 @@ const reachable = await z.isReachable(); // true | false
252
238
 
253
239
  ```typescript
254
240
  const info = await z.getDeviceInfo();
255
- // {
256
- // model: 'TCIV-2+',
257
- // firmware: '9.2.3.0',
258
- // mac: '00:13:cb:28:35:ca',
259
- // ip: '192.168.1.143',
260
- // hostname: 'zenitel01',
261
- // serialNumber: '22040000',
262
- // hardwareType: '8801',
263
- // mode: 'sip',
264
- // hasCamera: true,
265
- // sipDomain: 'testing-mo16m3gw.sip.twilio.com',
266
- // sipRegistered: true,
267
- // sipNumber: 'zenitel01',
268
- // outboundProxy: 'testing-mo16m3gw.sip.twilio.com',
269
- // uptime: 'up 2 days, 20 hours, 3 minutes',
270
- // webcallEnabled: true,
271
- // platform: 'turbine',
272
- // systemModelName: 'Turbine Compact - Video Plus'
273
- // }
241
+ // { model, firmware, mac, ip, hostname, serialNumber, hardwareType,
242
+ // mode, hasCamera, sipDomain, sipRegistered, webcallEnabled, ... }
274
243
  ```
275
244
 
276
245
  #### Webcall (Place / Stop / Answer calls)
277
246
 
278
247
  ```typescript
279
- await z.placeCall('122'); // Dial a number
280
- await z.placeCall('sip:user@domain');
281
- await z.stopCall(); // Hang up
282
- await z.answerCall(); // Answer incoming call
248
+ await z.placeCall('122');
249
+ await z.stopCall();
250
+ await z.answerCall();
283
251
  const status = await z.getCallStatus(); // 'Idle' | 'Calling' | 'Connected' | 'Ringing'
284
252
  ```
285
253
 
286
254
  #### Relay / Door Control
287
255
 
288
256
  ```typescript
289
- // Open door (relay1, 3 seconds)
290
257
  await z.activateRelay({ relayId: 'relay1', timer: 3 });
291
-
292
- // Activate GPIO output 2 for 5 seconds
293
258
  await z.activateRelay({ relayId: 'gpio2', timer: 5 });
294
-
295
- // Deactivate relay
296
259
  await z.deactivateRelay('relay1');
297
-
298
- // Get all relay statuses
299
260
  const relays = await z.getRelayStatus();
300
- // { relay1: 'Deactivated', gpio1: 'Deactivated', ..., gpio6: 'Deactivated' }
261
+ // { relay1: 'Deactivated', gpio1: ..., gpio6: ... }
301
262
  ```
302
263
 
303
- Available relay IDs: `relay1`, `gpio1`, `gpio2`, `gpio3`, `gpio4`, `gpio5`, `gpio6`.
264
+ Relay IDs: `relay1`, `gpio1`–`gpio6`
304
265
 
305
266
  #### SIP Configuration
306
267
 
307
268
  ```typescript
308
- // Read
309
269
  const sip = await z.getSIPConfig();
310
- // {
311
- // displayName: 'zenitel01',
312
- // directoryNumber: 'zenitel01',
313
- // domain: 'testing-mo16m3gw.sip.twilio.com',
314
- // authUsername: 'zenitel01',
315
- // authPassword: '********',
316
- // outboundProxy: 'testing-mo16m3gw.sip.twilio.com',
317
- // transport: 'UDP'
318
- // }
319
-
320
- // Write (partial updates supported)
321
- await z.setSIPConfig({
322
- domain: 'my-trunk.sip.twilio.com',
323
- directoryNumber: 'station01',
324
- outboundProxy: 'my-trunk.sip.twilio.com',
325
- });
270
+ // { displayName, directoryNumber, domain, authUsername, outboundProxy, transport }
326
271
 
327
- // Reboot required for SIP changes to take effect
328
- await z.reboot();
272
+ await z.setSIPConfig({ domain: 'my-trunk.sip.twilio.com' });
273
+ await z.reboot(); // Required for SIP changes
329
274
  ```
330
275
 
331
276
  #### Webcall Management
332
277
 
333
278
  ```typescript
334
- await z.enableWebcall(); // Enable HTTP webcall + relay API
335
- await z.disableWebcall(); // Disable it
279
+ await z.enableWebcall();
280
+ await z.disableWebcall();
336
281
  ```
337
282
 
338
- > **Note:** Firmware ≥4.11.3.1 disables webcall by default. You must enable it for relay and call control to work via HTTP.
283
+ > Firmware ≥4.11.3.1 disables webcall by default. Enable it for relay/call HTTP control.
339
284
 
340
285
  #### Config Backup & Restore
341
286
 
342
287
  ```typescript
343
- // Download full config as tar.gz
344
288
  const backup = await z.downloadConfig();
345
289
  fs.writeFileSync('backup.tar.gz', backup);
346
290
 
347
- // Upload config (apply on next reboot)
348
291
  const tarGz = fs.readFileSync('backup.tar.gz');
349
292
  await z.uploadConfig(tarGz);
350
293
  await z.reboot();
351
294
  ```
352
295
 
353
- The backup archive contains:
354
- - `config/ipst_config.xml` — Main configuration (SIP, relays, audio, network)
355
- - `config/zapconfig.json` — ZAP configuration
356
- - `config/snmpd.conf` — SNMP configuration
357
- - `config/certs/` — TLS certificates
358
- - `config/ui/logos/` — Custom logos
359
- - `config/ui/background_images/` — Background images
360
-
361
296
  #### Video
362
297
 
363
298
  ```typescript
364
- const mjpgUrl = z.getMJPGUrl();
365
- // "http://192.168.1.143/mjpg/video.mjpg"
366
- // Use in <img> tags for live feed (Basic Auth required)
367
-
368
- const rtspUrl = z.getRTSPUrl();
369
- // "rtsp://192.168.1.143:554/1/RTSP"
370
-
371
- const auth = z.getVideoAuth();
372
- // { user: 'admin', password: '***' }
373
- ```
374
-
375
- **MJPG in a browser/Electron:**
376
- ```html
377
- <!-- Direct (if no auth required or same-origin) -->
378
- <img src="http://192.168.1.143/mjpg/video.mjpg" />
379
-
380
- <!-- In Electron with protocol handler (recommended) -->
381
- <img src="portia-cam:///?ip=192.168.1.143&user=admin&pass=YOUR_PASS" />
299
+ z.getMJPGUrl(); // "http://192.168.1.143/mjpg/video.mjpg"
300
+ z.getRTSPUrl(); // "rtsp://192.168.1.143:554/1/RTSP"
301
+ z.getVideoAuth(); // { user: 'admin', password: '...' }
382
302
  ```
383
303
 
384
304
  #### Call Button (DAK) Configuration
385
305
 
386
- The call button is the physical button on the intercom that triggers a SIP call. Its target number is stored in the `ipst_config.xml` inside the device firmware.
387
-
388
306
  ```typescript
389
- // Read current call button configuration
390
307
  const dak = await z.readDAK();
391
- // {
392
- // number: '222',
393
- // domain: 'testing-mo16m3gw.sip.twilio.com',
394
- // raw: '222@testing-mo16m3gw.sip.twilio.com'
395
- // }
396
-
397
- // Configure call button to dial a random Portia agent
398
- // Flow: download backup → modify XML → upload → reboot
399
- await z.configureCallButton('portia-ae3c');
400
- // Domain auto-detected from current SIP config
308
+ // { number: '222', domain: 'sip.twilio.com', raw: '222@sip.twilio.com' }
401
309
 
402
- // With explicit domain + skip reboot
403
- await z.configureCallButton('portia-ae3c', 'my.sip.twilio.com', false);
310
+ await z.configureCallButton('portia-ae3c');
311
+ // Downloads backup → modifies XML → uploads → reboots (~2s)
404
312
  ```
405
313
 
406
- > **Under the hood:** `configureCallButton()` downloads the full config as `tar.gz`, uses a built-in tar parser (zero dependencies) to locate and modify `ipst_config.xml`, recalculates tar checksums, re-compresses, uploads, and optionally reboots. The entire process takes ~2 seconds.
407
-
408
314
  #### Full Device Provisioning
409
315
 
410
- One-shot setup for a factory-reset Zenitel. Configures SIP credentials, call button, webcall, and auto-answer in a single backup→upload→reboot cycle.
411
-
412
316
  ```typescript
413
- import type { ProvisionConfig } from '@pinecall/zenitel-client';
414
-
415
317
  await z.provisionDevice({
416
318
  sipDomain: 'testing-mo16m3gw.sip.twilio.com',
417
- sipAuthUser: 'your-sip-user',
319
+ sipAuthUser: 'zenitel01',
418
320
  sipAuthPassword: 'your-sip-password',
419
- agentNumber: 'portia-ae3c', // Call button will dial this
420
- // Optional:
421
- sipProxy: 'proxy.sip.twilio.com', // Default: sipDomain
422
- sipTransport: 'UDP', // Default: UDP
423
- stationName: 'Lobby Intercom', // Default: agentNumber
424
- enableWebcall: true, // Default: true
425
- autoAnswer: true, // Default: true
321
+ agentNumber: 'portia-ae3c',
322
+ enableWebcall: true, // default
323
+ autoAnswer: true, // default
426
324
  });
427
- // Device reboots automatically after upload
428
325
  ```
429
326
 
430
- **XML fields modified** (9 total):
431
- | XML Element | Value |
432
- |---|---|
433
- | `<sip_nick>` | stationName |
434
- | `<sip_id>` | stationName |
435
- | `<sip_domain>` | sipDomain |
436
- | `<sip_auth_user>` | sipAuthUser |
437
- | `<sip_auth_password>` | sipAuthPassword |
438
- | `<sip_outbound_transport>` | sipTransport |
439
- | `<sip_outbound_proxy_address>` | sipProxy |
440
- | `<dak1><val>` | agentNumber@sipDomain |
441
- | `<enable_wc_r>` | 1 |
442
- | `<auto_answer_mode>` | 1 |
327
+ #### Audio Settings
443
328
 
444
- #### Reboot
329
+ Read and write the full audio configuration — speaker/mic gain, echo cancellation, noise suppression, compression, and automatic volume control.
330
+
331
+ > The Zenitel config endpoint requires the **complete** JSON payload. `setAudioSettings()` handles this automatically — it reads, merges your changes, and writes back.
445
332
 
446
333
  ```typescript
447
- await z.reboot();
448
- // Device will be offline for approximately 30 seconds
449
- ```
334
+ const audio = await z.getAudioSettings();
335
+ // { speaker, mic, aec, anc, drc, avc, fess, lineOut, mode }
336
+
337
+ await z.setAudioSettings({ speaker: { gain: 3 } });
338
+ await z.setAudioSettings({ mic: { gain: 3 } });
339
+ await z.setAudioSettings({ drc: { enabled: true, gain: 8 } });
340
+ await z.setAudioSettings({
341
+ speaker: { gain: 2 },
342
+ aec: { enabled: true, mode: 'aggressive' },
343
+ });
450
344
 
451
- ---
345
+ // Backup raw config
346
+ const raw = await z.getAudioSettingsRaw();
347
+ fs.writeFileSync('audio-backup.json', JSON.stringify(raw, null, 2));
348
+ ```
452
349
 
453
- ### `scanNetwork()`
350
+ | Setting | Property | Range | Description |
351
+ |---------|----------|-------|-------------|
352
+ | Speaker Volume | `speaker.gain` | -10 to +13 dB | Playback level |
353
+ | Mic Sensitivity | `mic.gain` | -10 to +10 dB | Input level |
354
+ | Echo Cancel | `aec.enabled` / `.mode` | `moderate` · `aggressive` | Removes speaker bleed from mic |
355
+ | Noise Suppress | `anc.enabled` / `.mode` | `moderate` · `aggressive` | Filters ambient noise |
356
+ | Compression | `drc.enabled` / `.gain` | 0–20 dBA | Normalizes volume |
357
+ | Auto Volume | `avc.enabled` | bool | Adjusts to ambient noise |
358
+ | Squelch | `fess.enabled` / `.threshold` | -92–0 dBFS | Silences weak signals |
359
+ | Line Out | `lineOut.gain` | -20 to +20 dB | External speaker |
454
360
 
455
- Discovers Zenitel intercoms on the local network using multiple strategies.
361
+ #### Reboot
456
362
 
457
363
  ```typescript
458
- import { scanNetwork } from '@pinecall/zenitel-client';
459
-
460
- const devices = await scanNetwork();
461
- // [
462
- // {
463
- // ip: '192.168.1.143',
464
- // mac: '00:13:cb:28:35:ca',
465
- // model: 'TCIV-2+',
466
- // firmware: '9.2.3.0',
467
- // hasCamera: true,
468
- // hostname: 'zenitel01',
469
- // mode: 'sip'
470
- // }
471
- // ]
364
+ await z.reboot(); // ~30 seconds offline
472
365
  ```
473
366
 
474
- #### Scan Options
367
+ ---
475
368
 
476
- ```typescript
477
- const devices = await scanNetwork({
478
- timeout: 5000, // Scan timeout (ms)
479
- subnet: '192.168.1.0/24', // Auto-detected if omitted
480
- strategies: ['arp-oui', 'http-probe'] // Default: both
481
- });
482
- ```
369
+ ### `scanNetwork()`
483
370
 
484
- #### Discovery Strategies
371
+ ```typescript
372
+ import { scanNetwork } from 'zenitel-client';
485
373
 
486
- | Strategy | Method | Pros | Cons |
487
- |----------|--------|------|------|
488
- | **`arp-oui`** | Parse ARP table, filter by MAC prefix `00:13:CB` | Fast, no scanning | Requires recent ARP entry |
489
- | **`http-probe`** | Probe every IP in subnet, look for `zenitel.js` | Works without ARP cache | Slow on large subnets |
374
+ const devices = await scanNetwork({ timeout: 5000 });
375
+ // [{ ip, mac, model, firmware, hasCamera, hostname, mode }]
376
+ ```
490
377
 
491
- Results from all strategies are **merged by IP** — if the same device is found by multiple strategies, the entry with the most data wins.
378
+ | Strategy | Method | Speed |
379
+ |----------|--------|-------|
380
+ | `arp-oui` | ARP table + MAC prefix `00:13:CB` | Fast |
381
+ | `http-probe` | Probe every IP for `zenitel.js` | Thorough |
492
382
 
493
383
  ---
494
384
 
495
385
  ## Supported Hardware
496
386
 
497
- Confirmed on Zenitel Turbine platform (hardware type `8801`):
498
-
499
- | Model | Camera | Relay | Webcall | SIP Config | DAK | Firmware |
500
- |-------|--------|-------|---------|------------|-----|----------|
501
- | **TCIV-2+** | MJPG/RTSP | ✅ relay1 + gpio1–6 | ✅ | ✅ | | 9.2.3.0 |
502
- | TCIV-3+ | ✅ | ✅ | ✅ | ✅ | ✅ | Expected compatible |
503
- | TCIS-2 | ❌ | ✅ | ✅ | ✅ | ✅ | Expected compatible |
387
+ | Model | Camera | Relay | Webcall | Audio | Firmware |
388
+ |-------|--------|-------|---------|-------|----------|
389
+ | **TCIV-2+** | | relay1 + gpio1–6 | | | 9.2.3.0 |
390
+ | TCIV-3+ | ✅ | ✅ | ✅ | ✅ | Expected |
391
+ | TCIS-2 | | ✅ | ✅ | ✅ | Expected |
504
392
 
505
393
  ## Goform Endpoint Map
506
394
 
507
- All communication uses the Zenitel web UI's goform API:
508
-
509
395
  | Endpoint | Method | Purpose |
510
396
  |----------|--------|---------|
511
- | `/` | GET | Login page (302 redirect) |
512
- | `/goform/zForm_header` | GET | Device hidden fields (mode, platform, hwtype) |
513
- | `/goform/zForm_stn_info` | GET | Station info table (model, MAC, firmware, SIP status) |
397
+ | `/goform/zForm_header` | GET | Device metadata |
398
+ | `/goform/zForm_stn_info` | GET | Station info table |
514
399
  | `/goform/zForm_webcall` | GET/POST | Webcall + relay control |
515
- | `/goform/zForm_sip_configuration` | GET | SIP config form fields |
516
- | `/goform/zForm_save_changes` | POST | Write SIP configuration |
517
- | `/goform/zForm_config_backup` | POST | Config restore (multipart upload) |
400
+ | `/goform/zForm_sip_configuration` | GET | SIP config |
401
+ | `/goform/zForm_save_changes` | POST | Write SIP config |
402
+ | `/goform/zForm_audio_configuration` | GET | Audio config (JSON) |
403
+ | `/goform/zForm_auto_config` | POST | Write audio/DSP config |
404
+ | `/goform/zForm_config_backup` | POST | Config restore (upload) |
518
405
  | `/ipst_config.tar.gz` | GET | Config backup download |
519
406
  | `/goform/zForm_system_prefs` | POST | Reboot device |
520
- | `/mjpg/video.mjpg` | GET | Live MJPG video stream (port 80) |
407
+ | `/mjpg/video.mjpg` | GET | Live MJPG stream |
521
408
 
522
- Authentication: **HTTP Basic Auth** on all endpoints.
409
+ Auth: **HTTP Basic** on all endpoints.
523
410
 
524
411
  ## License
525
412
 
package/dist/cli.js CHANGED
@@ -297,6 +297,83 @@ Optional:
297
297
  console.log(` After reboot, call button will dial: ${agentNumber}`);
298
298
  break;
299
299
  }
300
+ case 'audio': {
301
+ const client = getClient();
302
+ const subCmd = args[1]; // get | set | backup
303
+ if (subCmd === 'set') {
304
+ const partial = {};
305
+ if (flag('speaker') !== undefined)
306
+ partial.speaker = { gain: Number(flag('speaker')) };
307
+ if (flag('mic') !== undefined)
308
+ partial.mic = { gain: Number(flag('mic')) };
309
+ if (hasFlag('aec-on'))
310
+ partial.aec = { enabled: true };
311
+ if (hasFlag('aec-off'))
312
+ partial.aec = { enabled: false };
313
+ if (hasFlag('anc-on'))
314
+ partial.anc = { enabled: true };
315
+ if (hasFlag('anc-off'))
316
+ partial.anc = { enabled: false };
317
+ if (hasFlag('drc-on'))
318
+ partial.drc = { enabled: true, gain: Number(flag('drc-gain') ?? 8) };
319
+ if (hasFlag('drc-off'))
320
+ partial.drc = { enabled: false };
321
+ if (hasFlag('avc-on'))
322
+ partial.avc = { enabled: true };
323
+ if (hasFlag('avc-off'))
324
+ partial.avc = { enabled: false };
325
+ if (Object.keys(partial).length === 0) {
326
+ console.error(`Usage: zenitel audio set -h <host> [options]
327
+
328
+ --speaker <dB> Speaker gain (-10 to +13)
329
+ --mic <dB> Mic gain (-10 to +10)
330
+ --aec-on/off Echo cancellation
331
+ --anc-on/off Noise suppression
332
+ --drc-on/off Dynamic compression (--drc-gain <0-20>)
333
+ --avc-on/off Auto volume`);
334
+ process.exit(1);
335
+ }
336
+ console.log('🔧 Updating audio settings...');
337
+ await client.setAudioSettings(partial);
338
+ console.log('✅ Audio settings applied.');
339
+ // Show updated values
340
+ const after = await client.getAudioSettings();
341
+ if (partial.speaker)
342
+ console.log(` Speaker: ${after.speaker.gain} dB`);
343
+ if (partial.mic)
344
+ console.log(` Mic: ${after.mic.gain} dB`);
345
+ if (partial.aec)
346
+ console.log(` AEC: ${after.aec.enabled ? 'ON' : 'OFF'}`);
347
+ if (partial.anc)
348
+ console.log(` ANC: ${after.anc.enabled ? 'ON' : 'OFF'}`);
349
+ if (partial.drc)
350
+ console.log(` DRC: ${after.drc.enabled ? 'ON' : 'OFF'} (${after.drc.gain} dBA)`);
351
+ if (partial.avc)
352
+ console.log(` AVC: ${after.avc.enabled ? 'ON' : 'OFF'}`);
353
+ }
354
+ else if (subCmd === 'backup') {
355
+ const { writeFileSync } = await import('node:fs');
356
+ const outFile = flag('out', 'o') || 'audio-backup.json';
357
+ console.log('💾 Backing up audio config...');
358
+ const raw = await client.getAudioSettingsRaw();
359
+ writeFileSync(outFile, JSON.stringify(raw, null, 2));
360
+ console.log(`✅ Saved to ${outFile}`);
361
+ }
362
+ else {
363
+ // Default: get / show
364
+ console.log(`🎵 Audio settings for ${flag('host', 'h')}:\n`);
365
+ const a = await client.getAudioSettings();
366
+ console.log(` Speaker: ${a.speaker.gain > 0 ? '+' : ''}${a.speaker.gain} dB (range: -10 to +13)`);
367
+ console.log(` Mic: ${a.mic.gain > 0 ? '+' : ''}${a.mic.gain} dB (range: -10 to +10)`);
368
+ console.log(` AEC: ${a.aec.enabled ? '✅ ON' : '❌ OFF'} (${a.aec.mode})`);
369
+ console.log(` ANC: ${a.anc.enabled ? '✅ ON' : '❌ OFF'} (${a.anc.mode})`);
370
+ console.log(` DRC: ${a.drc.enabled ? '✅ ON' : '❌ OFF'} (${a.drc.gain} dBA)`);
371
+ console.log(` AVC: ${a.avc.enabled ? '✅ ON' : '❌ OFF'}`);
372
+ console.log(` FESS: ${a.fess.enabled ? '✅ ON' : '❌ OFF'} (threshold: ${a.fess.threshold} dBFS)`);
373
+ console.log(` Mode: ${a.mode}`);
374
+ }
375
+ break;
376
+ }
300
377
  default:
301
378
  console.log(`zenitel — Zenitel intercom CLI
302
379
 
@@ -312,6 +389,9 @@ Commands:
312
389
  sip set -h <host> Write SIP config (--domain --number --proxy ...)
313
390
  webcall enable -h <host> Enable webcall + relay HTTP API
314
391
  webcall disable -h <host> Disable webcall + relay HTTP API
392
+ audio -h <host> Read audio settings
393
+ audio set -h <host> Write audio (--speaker --mic --aec-on/off ...)
394
+ audio backup -h <host> Backup audio config to JSON
315
395
  backup [file] -h <host> Download config as tar.gz
316
396
  restore <file> -h <host> Upload config tar.gz
317
397
  reboot -h <host> Reboot the device
@@ -329,6 +409,8 @@ Options:
329
409
  --number SIP directory number
330
410
  --proxy Outbound proxy
331
411
  --transport SIP transport (udp/tcp/tls)
412
+ --speaker Speaker gain in dB (-10 to +13)
413
+ --mic Mic gain in dB (-10 to +10)
332
414
  --name Display name`);
333
415
  }
334
416
  }
package/dist/client.js CHANGED
@@ -468,41 +468,24 @@ export class ZenitelClient {
468
468
  }
469
469
  /** Get raw audio config JSON from the device (for backup/debug) */
470
470
  async getAudioSettingsRaw() {
471
- // The audio config page embeds JSON in an AngularJS scope.
472
- // We fetch the page and extract the JSON from the script block.
473
- const html = await this._html('/goform/zForm_audio_configuration');
474
- // Strategy 1: Look for ng-init or inline JSON assignment
475
- // The page typically has: $scope.audio = {...} or data in a form field
476
- let jsonStr = '';
477
- // Try to extract from a script block: audio = {...}
478
- const scriptMatch = html.match(/audio\s*=\s*(\{[\s\S]*?\});\s*(?:\n|<\/script>)/)
479
- || html.match(/ng-init=['"]\s*init\(([\s\S]*?)\)['"]/);
480
- if (scriptMatch) {
481
- jsonStr = scriptMatch[1];
471
+ // The AngularJS audio controller fetches data via:
472
+ // POST /goform/zForm_auto_config
473
+ // body: get=get&path=/state/config/audio
474
+ // Response: { out: { get: { data: { audio: {...} } } } }
475
+ const body = new URLSearchParams({
476
+ get: 'get',
477
+ path: '/state/config/audio',
478
+ }).toString();
479
+ const res = await this._fetch('/goform/zForm_auto_config', 'POST', undefined, body, 'application/x-www-form-urlencoded');
480
+ // Zenitel appends a trailing form-feed (\f) after JSON — can't use res.json()
481
+ const text = (await res.text()).trim();
482
+ const json = JSON.parse(text);
483
+ // Response shape: { out: { get: { data: { audio: {...} } } } }
484
+ const audio = json?.out?.get?.data?.audio;
485
+ if (!audio) {
486
+ throw new Error('Unexpected response from audio config endpoint. Check firmware compatibility.');
482
487
  }
483
- else {
484
- // Strategy 2: POST to get the JSON directly (some FW versions)
485
- const res = await this._fetch('/goform/zForm_auto_config', 'GET');
486
- const text = await res.text();
487
- // The response may be JSON directly
488
- try {
489
- const parsed = JSON.parse(text);
490
- if (parsed.audio)
491
- return parsed;
492
- }
493
- catch { /* not JSON */ }
494
- // Strategy 3: Look for hidden input or textarea with JSON
495
- const inputMatch = html.match(/value='(\{&quot;audio&quot;[^']*)'/);
496
- if (inputMatch) {
497
- jsonStr = inputMatch[1]
498
- .replace(/&quot;/g, '"')
499
- .replace(/&amp;/g, '&');
500
- }
501
- }
502
- if (!jsonStr) {
503
- throw new Error('Could not extract audio config JSON from device. Firmware may not support this endpoint.');
504
- }
505
- return JSON.parse(jsonStr);
488
+ return { audio };
506
489
  }
507
490
  /** Parse the raw goform JSON into our typed AudioSettings */
508
491
  _parseAudioJson(raw) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zenitel-client",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "HTTP client + network scanner for Zenitel intercom systems (TCIV-2+, TCIV-3). Control relays, SIP configuration, DAK provisioning, webcall, and camera feeds.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",