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 +168 -281
- package/dist/cli.js +82 -0
- package/dist/client.js +17 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,75 @@
|
|
|
1
|
-
#
|
|
1
|
+
# zenitel-client
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/zenitel-client)
|
|
4
|
+
[](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
|
|
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
|
-
|
|
42
|
+
---
|
|
8
43
|
|
|
9
44
|
## Install
|
|
10
45
|
|
|
11
46
|
```bash
|
|
12
|
-
npm install
|
|
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
|
-
# 📋
|
|
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
|
-
|
|
73
|
-
zenitel
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
zenitel
|
|
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
|
|
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
|
|
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
|
-
|
|
131
|
-
zenitel backup -h 192.168.1.143
|
|
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
|
-
>
|
|
172
|
+
> Downloads config → modifies XML → re-uploads → reboots. Zero external dependencies.
|
|
174
173
|
|
|
175
174
|
### Full Device Provisioning
|
|
176
175
|
|
|
177
|
-
|
|
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
|
|
198
|
+
| `--host` | `-h` | Device IP address | required |
|
|
211
199
|
| `--user` | `-u` | Web UI username | `admin` |
|
|
212
|
-
| `--pass` | `-p` | Web UI password |
|
|
200
|
+
| `--pass` | `-p` | Web UI password | `alphaadmin` |
|
|
213
201
|
| `--id` | | Relay ID (`relay1`, `gpio1`–`gpio6`) | `relay1` |
|
|
214
|
-
| `--timer` | | Relay timer
|
|
215
|
-
| `--timeout` | | Scan timeout
|
|
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
|
|
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 '
|
|
221
|
+
import { ZenitelClient } from 'zenitel-client';
|
|
237
222
|
|
|
238
223
|
const z = new ZenitelClient({
|
|
239
224
|
host: '192.168.1.143',
|
|
240
|
-
user: 'admin',
|
|
241
|
-
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
|
-
//
|
|
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');
|
|
280
|
-
await z.
|
|
281
|
-
await z.
|
|
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:
|
|
261
|
+
// { relay1: 'Deactivated', gpio1: ..., gpio6: ... }
|
|
301
262
|
```
|
|
302
263
|
|
|
303
|
-
|
|
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
|
-
|
|
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();
|
|
335
|
-
await z.disableWebcall();
|
|
279
|
+
await z.enableWebcall();
|
|
280
|
+
await z.disableWebcall();
|
|
336
281
|
```
|
|
337
282
|
|
|
338
|
-
>
|
|
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
|
-
|
|
365
|
-
// "
|
|
366
|
-
//
|
|
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
|
-
|
|
403
|
-
|
|
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: '
|
|
319
|
+
sipAuthUser: 'zenitel01',
|
|
418
320
|
sipAuthPassword: 'your-sip-password',
|
|
419
|
-
agentNumber: 'portia-ae3c',
|
|
420
|
-
//
|
|
421
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
448
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
361
|
+
#### Reboot
|
|
456
362
|
|
|
457
363
|
```typescript
|
|
458
|
-
|
|
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
|
-
|
|
367
|
+
---
|
|
475
368
|
|
|
476
|
-
|
|
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
|
-
|
|
371
|
+
```typescript
|
|
372
|
+
import { scanNetwork } from 'zenitel-client';
|
|
485
373
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
|
500
|
-
|
|
501
|
-
|
|
|
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
|
-
|
|
|
512
|
-
| `/goform/
|
|
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
|
|
516
|
-
| `/goform/zForm_save_changes` | POST | Write SIP
|
|
517
|
-
| `/goform/
|
|
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
|
|
407
|
+
| `/mjpg/video.mjpg` | GET | Live MJPG stream |
|
|
521
408
|
|
|
522
|
-
|
|
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
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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='(\{"audio"[^']*)'/);
|
|
496
|
-
if (inputMatch) {
|
|
497
|
-
jsonStr = inputMatch[1]
|
|
498
|
-
.replace(/"/g, '"')
|
|
499
|
-
.replace(/&/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.
|
|
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",
|