This commit is contained in:
Mateusz Gruszczyński
2026-09-14 23:10:17 +02:00
parent 4bb9c8621a
commit 0a2fb8c6c7
47 changed files with 1337 additions and 372 deletions
+137 -11
View File
@@ -698,7 +698,7 @@ async function openDeviceDetails(id) {
const device = app.devices.find(item => item.id === id); if (!device) return;
const form = $('#deviceDetailsForm'); if (!form) return;
form.reset();
form.id.value = device.id;
form.elements.id.value = device.id;
form.name.value = device.name || '';
form.energy_source.value = device.energy_source || 'auto';
const title = $('#deviceDetailsTitle');
@@ -707,10 +707,24 @@ async function openDeviceDetails(id) {
if (meta) meta.innerHTML = device.connection_type === 'gree_cloud'
? `<div><span>${esc(tr('devices.connection'))}</span><strong>GREE Cloud</strong></div><div><span>${esc(tr('devices.transport'))}</span><strong>MQTT / TLS</strong></div><div><span>${esc(tr('devices.cloudDeviceId'))}</span><strong>${esc(device.cloud_device_id || device.mac || '—')}</strong></div><div><span>MAC</span><strong>${esc(device.mac || '—')}</strong></div>`
: `<div><span>${esc(tr('devices.connection'))}</span><strong>Local</strong></div><div><span>${esc(tr('devices.address'))}</span><strong>${esc(device.ip || '—')}:${esc(device.port || 7000)}</strong></div><div><span>MAC</span><strong>${esc(device.mac || '—')}</strong></div>`;
const installation = deviceInstallationForDevice(device.id);
const groupNote = $('#deviceEnergyGroupNote');
const groupOwnsEnergy = !!installation && (!!installation.energy_device_id || !!installation.ha_energy_entity_id);
if (groupNote) {
groupNote.hidden = !groupOwnsEnergy;
groupNote.innerHTML = groupOwnsEnergy
? `<span>${esc(deviceInstallationKindLabel(installation))}</span><strong>${esc(tr('devices.groupedEnergyNote', { name: installation.name, source: installationEnergySourceLabel(installation) }))}</strong>`
: '';
}
const source = form.energy_source;
if (groupOwnsEnergy) source.value = installation.energy_source || 'auto';
source.disabled = groupOwnsEnergy;
const cloudOption = [...source.options].find(option => option.value === 'gree_cloud');
if (cloudOption) cloudOption.disabled = device.capabilities?.energy_meter !== true;
const sensorSelect = form.ha_energy_entity_id;
sensorSelect.disabled = groupOwnsEnergy;
sensorSelect.innerHTML = '<option value="">—</option>';
try {
const response = await api('/api/integrations/home-assistant/energy-sensors');
@@ -723,23 +737,135 @@ async function openDeviceDetails(id) {
option.dataset.stateClass = sensor.state_class || '';
sensorSelect.append(option);
}
} catch (_) {
// Home Assistant is optional; keep any already-saved entity available even when HA is offline.
}
if (device.ha_energy_entity_id && ![...sensorSelect.options].some(option => option.value === device.ha_energy_entity_id)) {
} catch (_) { }
const energyEntity = groupOwnsEnergy ? installation.ha_energy_entity_id : device.ha_energy_entity_id;
const energyUnit = groupOwnsEnergy ? installation.ha_energy_unit : device.ha_energy_unit;
const energyDeviceClass = groupOwnsEnergy ? installation.ha_energy_device_class : device.ha_energy_device_class;
const energyStateClass = groupOwnsEnergy ? installation.ha_energy_state_class : device.ha_energy_state_class;
if (energyEntity && ![...sensorSelect.options].some(option => option.value === energyEntity)) {
const option = document.createElement('option');
option.value = device.ha_energy_entity_id;
option.textContent = device.ha_energy_entity_id;
option.dataset.unit = device.ha_energy_unit || '';
option.dataset.deviceClass = device.ha_energy_device_class || '';
option.dataset.stateClass = device.ha_energy_state_class || '';
option.value = energyEntity;
option.textContent = energyEntity;
option.dataset.unit = energyUnit || '';
option.dataset.deviceClass = energyDeviceClass || '';
option.dataset.stateClass = energyStateClass || '';
sensorSelect.append(option);
}
sensorSelect.value = device.ha_energy_entity_id || '';
sensorSelect.value = energyEntity || '';
updateDeviceEnergySensorMeta();
openDialog('deviceDetailsDialog');
}
function renderDeviceGroupsDialogList() {
const host = $('#deviceGroupsList'); if (!host) return;
const groups = app.deviceGroups || [];
host.innerHTML = groups.length ? groups.map(group => {
const members = (group.device_ids || []).map(id => app.devices.find(device => device.id === id)?.name).filter(Boolean);
return `<div class="installation-list-row"><button type="button" data-action="edit-device-group" data-id="${esc(group.id)}"><strong>${esc(group.name)}</strong><small>${esc(deviceInstallationKindLabel(group))} · ${esc(members.join(' · ') || '—')} · ${esc(installationEnergySourceLabel(group))}</small></button><div><button type="button" class="secondary" data-action="edit-device-group" data-id="${esc(group.id)}">${esc(tr('actions.edit'))}</button><button type="button" class="danger" data-action="delete-device-group" data-id="${esc(group.id)}">${esc(tr('actions.delete'))}</button></div></div>`;
}).join('') : `<div class="empty"><strong>${esc(tr('devices.newInstallation'))}</strong><span>${esc(tr('devices.installationsHint'))}</span></div>`;
}
function selectedDeviceGroupIds() {
return $$('#deviceGroupDeviceChoices input[type="checkbox"]:checked').map(input => input.value);
}
function syncDeviceGroupMemberSelects() {
const form = $('#deviceGroupForm'); if (!form) return;
const ids = selectedDeviceGroupIds();
const energy = form.energy_device_id;
const outdoor = form.outdoor_temperature_device_id;
const oldEnergy = energy.value, oldOutdoor = outdoor.value;
energy.innerHTML = '<option value="">—</option>' + ids.map(id => {
const device = app.devices.find(item => item.id === id);
if (!device) return '';
const supported = device.connection_type === 'gree_cloud' && device.capabilities?.energy_meter === true;
return `<option value="${esc(id)}" ${supported ? '' : 'disabled'}>${esc(device.name)}${supported ? '' : ' · —'}</option>`;
}).join('');
outdoor.innerHTML = `<option value="">${esc(tr('devices.noSharedOutdoor'))}</option>` + ids.map(id => {
const device = app.devices.find(item => item.id === id);
return device ? `<option value="${esc(id)}">${esc(device.name)}</option>` : '';
}).join('');
if ([...energy.options].some(option => option.value === oldEnergy && !option.disabled)) energy.value = oldEnergy;
if ([...outdoor.options].some(option => option.value === oldOutdoor)) outdoor.value = oldOutdoor;
}
function renderDeviceGroupDeviceChoices(selectedIds = []) {
const form = $('#deviceGroupForm');
const host = $('#deviceGroupDeviceChoices'); if (!form || !host) return;
const currentId = form.elements.id.value;
const occupied = new Map();
for (const group of (app.deviceGroups || [])) {
if (group.id === currentId) continue;
for (const id of (group.device_ids || [])) occupied.set(id, group.name);
}
host.innerHTML = app.devices.map(device => {
const owner = occupied.get(device.id);
const checked = selectedIds.includes(device.id);
return `<label class="check ${owner ? 'disabled' : ''}" title="${owner ? esc(owner) : ''}"><input type="checkbox" name="device_ids" value="${esc(device.id)}" ${checked ? 'checked' : ''} ${owner ? 'disabled' : ''}> <span>${esc(device.name)}${owner ? ` · ${esc(owner)}` : ''}</span></label>`;
}).join('');
syncDeviceGroupMemberSelects();
}
async function loadDeviceGroupEnergySensors(group = null) {
const form = $('#deviceGroupForm'); if (!form) return;
const select = form.ha_energy_entity_id;
select.innerHTML = '<option value="">—</option>';
try {
const response = await api('/api/integrations/home-assistant/energy-sensors');
for (const sensor of (response.sensors || [])) {
const option = document.createElement('option');
option.value = sensor.entity_id;
option.textContent = `${sensor.name || sensor.entity_id} · ${sensor.unit || ''}`;
option.dataset.unit = sensor.unit || '';
option.dataset.deviceClass = sensor.device_class || '';
option.dataset.stateClass = sensor.state_class || '';
select.append(option);
}
} catch (_) { }
if (group?.ha_energy_entity_id && ![...select.options].some(option => option.value === group.ha_energy_entity_id)) {
const option = document.createElement('option');
option.value = group.ha_energy_entity_id;
option.textContent = group.ha_energy_entity_id;
option.dataset.unit = group.ha_energy_unit || '';
option.dataset.deviceClass = group.ha_energy_device_class || '';
option.dataset.stateClass = group.ha_energy_state_class || '';
select.append(option);
}
select.value = group?.ha_energy_entity_id || '';
updateDeviceGroupEnergySensorMeta();
}
async function populateDeviceGroupForm(group = null) {
const form = $('#deviceGroupForm'); if (!form) return;
form.reset();
form.elements.id.value = group?.id || '';
form.name.value = group?.name || '';
form.kind.value = group?.kind || 'split';
form.energy_source.value = group?.energy_source || 'auto';
renderDeviceGroupDeviceChoices(group?.device_ids || []);
syncDeviceGroupMemberSelects();
form.energy_device_id.value = group?.energy_device_id || '';
form.outdoor_temperature_device_id.value = group?.outdoor_temperature_device_id || '';
await loadDeviceGroupEnergySensors(group);
}
async function openDeviceGroupsDialog(groupId = '') {
renderDeviceGroupsDialogList();
const group = groupId ? (app.deviceGroups || []).find(item => item.id === groupId) : null;
await populateDeviceGroupForm(group || null);
openDialog('deviceGroupsDialog');
}
function updateDeviceGroupEnergySensorMeta() {
const select = $('#deviceGroupForm')?.ha_energy_entity_id;
const meta = $('#deviceGroupEnergySensorMeta');
if (!select || !meta) return;
const option = select.selectedOptions?.[0];
meta.textContent = option?.value
? `${option.value} · ${option.dataset.deviceClass || 'energy'} · ${option.dataset.stateClass || 'total'} · ${option.dataset.unit || 'kWh'}`
: tr('energy.noHaSensor');
}
function updateDeviceEnergySensorMeta() {
const select = $('#deviceDetailsForm')?.ha_energy_entity_id;
const meta = $('#deviceEnergySensorMeta');