feat(call-home): Implement Auto Call Home configuration management
- Added `CallHomeConfig` model to represent the Auto Call Home settings. - Introduced methods in `MiniMateClient` for reading (`get_call_home_config`) and writing (`set_call_home_config`) the call home configuration. - Updated `MiniMateProtocol` with new commands for call home operations (SUB 0x2C for read, SUB 0x7E for write, and SUB 0x7F for confirm). - Created API endpoints for retrieving and updating call home settings in the server. - Enhanced the web interface with a new "Call Home" tab for user interaction with call home settings. - Implemented JavaScript functions for reading and writing call home configurations from the web app.
This commit is contained in:
+249
-3
@@ -736,9 +736,10 @@
|
||||
|
||||
<!-- ── Live tab bar ───────────────────────────────────────────────── -->
|
||||
<div class="tab-bar" id="live-tab-bar">
|
||||
<button class="tab-btn active" data-tab="device" onclick="switchTab('device')">Device</button>
|
||||
<button class="tab-btn" data-tab="events" onclick="switchTab('events')">Events</button>
|
||||
<button class="tab-btn" data-tab="config" onclick="switchTab('config')">Config</button>
|
||||
<button class="tab-btn active" data-tab="device" onclick="switchTab('device')">Device</button>
|
||||
<button class="tab-btn" data-tab="events" onclick="switchTab('events')">Events</button>
|
||||
<button class="tab-btn" data-tab="config" onclick="switchTab('config')">Config</button>
|
||||
<button class="tab-btn" data-tab="call-home" onclick="switchTab('call-home')">Call Home</button>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════
|
||||
@@ -909,6 +910,123 @@
|
||||
|
||||
</div><!-- end #tab-config -->
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════
|
||||
TAB: Call Home
|
||||
═══════════════════════════════════════════════════════════════════ -->
|
||||
<div id="tab-call-home" class="tab-pane" style="display:none">
|
||||
|
||||
<div class="cfg-grid">
|
||||
|
||||
<!-- Enable / dial -->
|
||||
<div class="cfg-section">
|
||||
<div class="cfg-section-title">Auto Call Home</div>
|
||||
|
||||
<div class="cfg-field">
|
||||
<label>Enable Auto Call Home</label>
|
||||
<select id="ch-enabled">
|
||||
<option value="">— unchanged —</option>
|
||||
<option value="true">Enabled</option>
|
||||
<option value="false">Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="cfg-field">
|
||||
<label>Dial String</label>
|
||||
<input type="text" id="ch-dial-string" disabled placeholder="Read-only (e.g. RADIO RING)" />
|
||||
<div class="hint">Read from device — not writable via this interface</div>
|
||||
</div>
|
||||
|
||||
<div class="cfg-section-title" style="margin-top:16px">When to Call</div>
|
||||
|
||||
<div class="cfg-field">
|
||||
<label>After Event Recorded</label>
|
||||
<select id="ch-after-event">
|
||||
<option value="">— unchanged —</option>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="cfg-field">
|
||||
<label>At Specified Times</label>
|
||||
<select id="ch-at-times">
|
||||
<option value="">— unchanged —</option>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Scheduled call times -->
|
||||
<div class="cfg-section">
|
||||
<div class="cfg-section-title">Scheduled Call Times</div>
|
||||
|
||||
<div class="cfg-field">
|
||||
<label>Time Slot 1</label>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<select id="ch-t1-enabled" style="width:120px">
|
||||
<option value="">— enable —</option>
|
||||
<option value="true">Enabled</option>
|
||||
<option value="false">Disabled</option>
|
||||
</select>
|
||||
<input type="number" id="ch-t1-hour" min="0" max="23" step="1" placeholder="HH" style="width:64px" />
|
||||
<span>:</span>
|
||||
<input type="number" id="ch-t1-min" min="0" max="59" step="1" placeholder="MM" style="width:64px" />
|
||||
</div>
|
||||
<div class="hint">Hour (0-23) and minute (0-59). Avoid value 3 (DLE limitation).</div>
|
||||
</div>
|
||||
|
||||
<div class="cfg-field">
|
||||
<label>Time Slot 2</label>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<select id="ch-t2-enabled" style="width:120px">
|
||||
<option value="">— enable —</option>
|
||||
<option value="true">Enabled</option>
|
||||
<option value="false">Disabled</option>
|
||||
</select>
|
||||
<input type="number" id="ch-t2-hour" min="0" max="23" step="1" placeholder="HH" style="width:64px" />
|
||||
<span>:</span>
|
||||
<input type="number" id="ch-t2-min" min="0" max="59" step="1" placeholder="MM" style="width:64px" />
|
||||
</div>
|
||||
<div class="hint">Hour (0-23) and minute (0-59). Avoid value 3 (DLE limitation).</div>
|
||||
</div>
|
||||
|
||||
<div class="cfg-section-title" style="margin-top:16px">Retry Settings (read-only)</div>
|
||||
|
||||
<div class="cfg-field">
|
||||
<label>Number of Retries</label>
|
||||
<input type="text" id="ch-num-retries" disabled placeholder="—" />
|
||||
</div>
|
||||
|
||||
<div class="cfg-field">
|
||||
<label>Time Between Retries (s)</label>
|
||||
<input type="text" id="ch-retry-gap" disabled placeholder="—" />
|
||||
</div>
|
||||
|
||||
<div class="cfg-field">
|
||||
<label>Wait for Connection (s)</label>
|
||||
<input type="text" id="ch-wait-conn" disabled placeholder="—" />
|
||||
</div>
|
||||
|
||||
<div class="cfg-field">
|
||||
<label>Warm-up Time (s)</label>
|
||||
<input type="text" id="ch-warmup" disabled placeholder="—" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="cfg-actions">
|
||||
<button class="btn btn-ghost" id="ch-read-btn" onclick="readCallHome()" disabled>Read from Device</button>
|
||||
<button class="btn btn-success" id="ch-write-btn" onclick="writeCallHome()" disabled>Write to Device</button>
|
||||
<button class="btn btn-ghost" onclick="clearCallHomeForm()">Clear Form</button>
|
||||
<span id="ch-status"></span>
|
||||
</div>
|
||||
|
||||
</div><!-- end #tab-call-home -->
|
||||
|
||||
</div><!-- end #section-live -->
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════
|
||||
@@ -1494,6 +1612,134 @@ async function writeConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Call Home form ─────────────────────────────────────────────────────────────
|
||||
function setChStatus(msg, type) {
|
||||
const el = document.getElementById('ch-status');
|
||||
el.textContent = msg;
|
||||
el.style.color = type === 'ok' ? '#4caf50' : type === 'error' ? '#f44336' : '#aaa';
|
||||
}
|
||||
|
||||
function populateCallHomeForm(ch) {
|
||||
if (!ch) return;
|
||||
const qs2 = id => document.getElementById(id);
|
||||
|
||||
// Read-only display fields
|
||||
if (ch.dial_string != null) qs2('ch-dial-string').value = ch.dial_string || '';
|
||||
if (ch.num_retries != null) qs2('ch-num-retries').value = ch.num_retries;
|
||||
if (ch.time_between_retries_sec != null) qs2('ch-retry-gap').value = ch.time_between_retries_sec;
|
||||
if (ch.wait_for_connection_sec != null) qs2('ch-wait-conn').value = ch.wait_for_connection_sec;
|
||||
if (ch.warm_up_time_sec != null) qs2('ch-warmup').value = ch.warm_up_time_sec;
|
||||
|
||||
// Editable select/input fields (use "" for "unchanged" state when value is null)
|
||||
function setBool(id, val) {
|
||||
if (val != null) document.getElementById(id).value = val ? 'true' : 'false';
|
||||
}
|
||||
setBool('ch-enabled', ch.auto_call_home_enabled);
|
||||
setBool('ch-after-event', ch.after_event_recorded);
|
||||
setBool('ch-at-times', ch.at_specified_times);
|
||||
setBool('ch-t1-enabled', ch.time1_enabled);
|
||||
setBool('ch-t2-enabled', ch.time2_enabled);
|
||||
if (ch.time1_hour != null) qs2('ch-t1-hour').value = ch.time1_hour;
|
||||
if (ch.time1_min != null) qs2('ch-t1-min').value = ch.time1_min;
|
||||
if (ch.time2_hour != null) qs2('ch-t2-hour').value = ch.time2_hour;
|
||||
if (ch.time2_min != null) qs2('ch-t2-min').value = ch.time2_min;
|
||||
}
|
||||
|
||||
function clearCallHomeForm() {
|
||||
['ch-enabled','ch-after-event','ch-at-times','ch-t1-enabled','ch-t2-enabled']
|
||||
.forEach(id => { document.getElementById(id).selectedIndex = 0; });
|
||||
['ch-t1-hour','ch-t1-min','ch-t2-hour','ch-t2-min']
|
||||
.forEach(id => { document.getElementById(id).value = ''; });
|
||||
// Keep read-only display fields but clear them too
|
||||
['ch-dial-string','ch-num-retries','ch-retry-gap','ch-wait-conn','ch-warmup']
|
||||
.forEach(id => { document.getElementById(id).value = ''; });
|
||||
setChStatus('');
|
||||
}
|
||||
|
||||
async function readCallHome() {
|
||||
if (!devHost()) { setChStatus('Not connected.', 'error'); return; }
|
||||
setChStatus('Reading call home config from device…');
|
||||
document.getElementById('ch-read-btn').disabled = true;
|
||||
try {
|
||||
const r = await fetch(`${api()}/device/call_home?${deviceParams()}`);
|
||||
if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.detail || r.statusText); }
|
||||
const ch = await r.json();
|
||||
populateCallHomeForm(ch);
|
||||
setChStatus('Call home config loaded from device.', 'ok');
|
||||
} catch(e) {
|
||||
setChStatus(`Read failed: ${e.message}`, 'error');
|
||||
} finally {
|
||||
document.getElementById('ch-read-btn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeCallHome() {
|
||||
if (!devHost()) { setChStatus('Not connected.', 'error'); return; }
|
||||
|
||||
// Build body — only include fields that have values
|
||||
const body = {};
|
||||
|
||||
function getBool(id) {
|
||||
const v = document.getElementById(id).value;
|
||||
return v === '' ? null : v === 'true';
|
||||
}
|
||||
function getIntField(id) {
|
||||
const v = document.getElementById(id).value.trim();
|
||||
return v === '' ? null : parseInt(v, 10);
|
||||
}
|
||||
|
||||
const en = getBool('ch-enabled');
|
||||
if (en !== null) body.auto_call_home_enabled = en;
|
||||
const ae = getBool('ch-after-event');
|
||||
if (ae !== null) body.after_event_recorded = ae;
|
||||
const at = getBool('ch-at-times');
|
||||
if (at !== null) body.at_specified_times = at;
|
||||
const t1e = getBool('ch-t1-enabled');
|
||||
if (t1e !== null) body.time1_enabled = t1e;
|
||||
const t1h = getIntField('ch-t1-hour');
|
||||
if (t1h !== null) body.time1_hour = t1h;
|
||||
const t1m = getIntField('ch-t1-min');
|
||||
if (t1m !== null) body.time1_min = t1m;
|
||||
const t2e = getBool('ch-t2-enabled');
|
||||
if (t2e !== null) body.time2_enabled = t2e;
|
||||
const t2h = getIntField('ch-t2-hour');
|
||||
if (t2h !== null) body.time2_hour = t2h;
|
||||
const t2m = getIntField('ch-t2-min');
|
||||
if (t2m !== null) body.time2_min = t2m;
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
setChStatus('No fields to write — change at least one field.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Warn about value 3 in hour/min fields
|
||||
const hourMinFields = [body.time1_hour, body.time1_min, body.time2_hour, body.time2_min];
|
||||
if (hourMinFields.some(v => v === 3)) {
|
||||
setChStatus('Error: value 3 in hour/minute fields is not supported (DLE protocol limitation).', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldsStr = Object.keys(body).join(', ');
|
||||
setChStatus(`Writing ${Object.keys(body).length} field(s)…`);
|
||||
document.getElementById('ch-write-btn').disabled = true;
|
||||
|
||||
try {
|
||||
const r = await fetch(`${api()}/device/call_home?${deviceParams()}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.detail || r.statusText); }
|
||||
setChStatus(`Written: ${fieldsStr}`, 'ok');
|
||||
// Re-read to confirm changes
|
||||
await readCallHome();
|
||||
} catch(e) {
|
||||
setChStatus(`Write failed: ${e.message}`, 'error');
|
||||
} finally {
|
||||
document.getElementById('ch-write-btn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Events ─────────────────────────────────────────────────────────────────────
|
||||
function populateEventChips() {
|
||||
const el = document.getElementById('event-chips');
|
||||
|
||||
Reference in New Issue
Block a user