zenitel-client 0.1.1 → 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/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.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",