This commit is contained in:
Mateusz Gruszczyński
2026-09-18 11:21:48 +02:00
parent 3ffb02595e
commit 8d96ad2d38
29 changed files with 758 additions and 256 deletions
+28
View File
@@ -1538,6 +1538,34 @@ legend {
overflow-wrap: anywhere;
}
.local-discovery-row {
gap: 10px;
}
.local-discovery-head {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
}
.local-discovery-head > input[type="checkbox"] {
width: 18px;
height: 18px;
margin: 0;
}
.local-discovery-head > label {
display: grid;
gap: 2px;
min-width: 0;
cursor: pointer;
}
.local-discovery-head > .badge {
justify-self: end;
}
* {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--accent) 42%, var(--surface-2)) var(--surface-muted);
+5 -7
View File
@@ -1263,8 +1263,7 @@
<h2 data-i18n="discovery.title">Discover GREE devices</h2><button type="button" data-close
data-ui-icon="close"></button>
</div>
<p class="field-note" data-i18n="discovery.help">Auto searches both AES-ECB and AES-GCM devices. Multiple passes
improve discovery when several Wi-Fi modules answer the same broadcast.</p>
<p class="field-note" data-i18n="discovery.help">Auto accepts both AES-ECB and AES-GCM devices. Selecting V1 or V2 accepts and binds only that protocol. Multiple passes improve discovery when several Wi-Fi modules answer the same broadcast.</p>
<label><span data-i18n="discovery.protocol">Discovery protocol</span><select name="protocol_version">
<option value="0" data-i18n="devices.protocolAuto">Auto (V1 + V2)</option>
<option value="1">V1 AES-ECB</option>
@@ -1282,15 +1281,14 @@
<dialog id="discoveryNamesDialog">
<form method="dialog" id="discoveryNamesForm" class="dialog-form">
<div class="dialog-head">
<h2 data-i18n="discovery.nameDevices">Name discovered devices</h2><button type="button" data-close
<h2 data-i18n="discovery.nameDevices">Discovered local devices</h2><button type="button" data-close
data-ui-icon="close"></button>
</div>
<p class="field-note" data-i18n="discovery.nameDevicesHelp">Give each new unit a friendly room name. The technical
model and MAC remain available in diagnostics.</p>
<p class="field-note" data-i18n="discovery.nameDevicesHelp">Select the units you want to add. MAC address, IP address and detected protocol are shown for every result.</p>
<div id="discoveryNamesList" class="discovery-name-list"></div>
<div class="form-actions"><button type="button" class="secondary" data-close
data-i18n="actions.later">Later</button><button class="primary" type="submit"
data-i18n="actions.save">Save</button></div>
data-i18n="actions.cancel">Cancel</button><button class="primary" type="submit"
data-i18n="discovery.addSelected">Add selected</button></div>
</form>
</dialog>
+1
View File
@@ -37,6 +37,7 @@ const app = {
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main', settingsTab: 'app', systemSnapshotAt: Date.now(),
pingMonitor: { targetId: '', all: false, running: false, timer: null, inFlight: false, samples: {} },
flowDraft: null, flowSelectedNodeId: null, flowSelectedNodeIds: [], flowConnectFrom: null, flowDirty: false, flowZoom: 1,
localDiscoveryCandidates: [],
};
try { app.savedCharts = JSON.parse(localStorage.getItem('gree_controller_saved_charts') || '[]'); } catch (_) { app.savedCharts = []; }
+24 -7
View File
@@ -366,22 +366,39 @@ $('#tokenForm').addEventListener('submit', async event => {
$('#discoverForm').addEventListener('submit', async event => {
event.preventDefault(); const form = event.currentTarget, raw = Object.fromEntries(new FormData(form));
await runFormTask(form, async () => {
const result = await api('/api/discovery', { method: 'POST', body: { protocol_version: Number(raw.protocol_version), passes: Number(raw.passes), timeout_ms: Number(raw.timeout_ms) } });
form.closest('dialog').close(); await loadBootstrap(); toast(tr('toast.found', { count: result.count })); showDiscoveryNames(result.new_device_ids || []);
const protocolVersion = Number(raw.protocol_version);
const result = await api('/api/discovery/scan', { method: 'POST', body: { protocol_version: protocolVersion, passes: Number(raw.passes), timeout_ms: Number(raw.timeout_ms) } });
form.closest('dialog').close();
toast(tr('toast.found', { count: result.count }));
showDiscoveryNames(result.devices || []);
}, { busyKey: 'actions.discovering' });
});
$('#discoveryNamesForm').addEventListener('submit', async event => {
event.preventDefault();
const form = event.currentTarget;
const inputs = $$('input[data-device-id]', form);
const selected = $$('[data-discovery-select]:checked', form);
if (!selected.length) {
toast(tr('discovery.selectAtLeastOne'), true);
return;
}
await runFormTask(form, async () => {
const updated = await Promise.all(inputs.map(input => api(`/api/devices/${encodeURIComponent(input.dataset.deviceId)}`, { method: 'PATCH', body: { name: input.value.trim() } })));
updated.forEach(updateDevice);
const devices = selected.map(input => {
const index = Number(input.dataset.discoveryIndex);
const candidate = app.localDiscoveryCandidates[index];
const nameInput = $(`[data-discovery-name="${index}"]`, form);
return { ...candidate, name: String(nameInput?.value || candidate?.name || '').trim() };
}).filter(Boolean);
const result = await api('/api/discovery/add', {
method: 'POST',
body: { devices },
});
await loadBootstrap();
form.closest('dialog').close();
app.localDiscoveryCandidates = [];
renderAll();
toast(tr('common.saved'));
});
toast(tr('discovery.addedCount', { count: result.count }));
}, { validate: false });
});
$('#renameDeviceForm').addEventListener('submit', async event => {
+20 -9
View File
@@ -469,16 +469,27 @@ function beginInlineTemperatureEdit(target) {
input.addEventListener('blur', () => finish(true), { once: true });
}
function showDiscoveryNames(ids) {
const wanted = new Set(Array.isArray(ids) ? ids : []);
const devices = app.devices.filter(device => wanted.has(device.id));
if (!devices.length) return;
function showDiscoveryNames(devices) {
const items = Array.isArray(devices) ? devices : [];
app.localDiscoveryCandidates = items.map(item => ({ ...item }));
const list = $('#discoveryNamesList');
list.innerHTML = devices.map(device => `
<label class="discovery-name-row">
<span><strong>${esc(device.model || 'GREE')}</strong><small>${esc(device.ip)} · ${esc(device.mac)} · ${device.protocol_version === 2 ? 'V2 GCM' : 'V1 ECB'}</small></span>
<input data-device-id="${esc(device.id)}" maxlength="80" required value="${esc(device.name)}">
</label>`).join('');
if (!list) return;
list.innerHTML = items.length ? items.map((device, index) => {
const alreadyAdded = device.already_added === true;
const protocol = Number(device.protocol_version) === 2 ? 'V2 AES-GCM' : 'V1 AES-ECB';
const selectorId = `local-discovery-${index}`;
return `<div class="discovery-name-row local-discovery-row">
<div class="local-discovery-head">
<input type="checkbox" id="${esc(selectorId)}" data-discovery-select data-discovery-index="${index}" ${alreadyAdded ? 'disabled' : 'checked'}>
<label for="${esc(selectorId)}">
<strong>${esc(device.model || device.name || 'GREE')}</strong>
<small>IP ${esc(device.ip || '—')} · MAC ${esc(device.mac || '—')} · ${esc(protocol)}</small>
</label>
${alreadyAdded ? `<span class="badge">${esc(tr('discovery.alreadyAdded'))}</span>` : ''}
</div>
<input data-discovery-name="${index}" maxlength="80" value="${esc(device.name || device.model || 'GREE')}" ${alreadyAdded ? 'disabled' : ''} aria-label="${esc(tr('common.name'))}">
</div>`;
}).join('') : `<div class="empty"><strong>${esc(tr('discovery.empty'))}</strong><span>${esc(tr('discovery.emptyHint'))}</span></div>`;
openDialog('discoveryNamesDialog');
}