240 lines
19 KiB
JavaScript
240 lines
19 KiB
JavaScript
function renderHistorySummary() {
|
||
const host = $('#historySummary'); if (!host) return;
|
||
if (app.historyTab === 'energy') { renderEnergyHistorySummary(); return; }
|
||
const deviceRows = app.historyData.devices, zoneRows = app.historyData.zones, sensorRows = app.historyData.sensors;
|
||
const cards = [
|
||
[tr('history.deviceSamples'), app.historyCounts.devices ?? deviceRows.length, tr('history.greeHistory')],
|
||
[tr('history.zoneSamples'), app.historyCounts.zones ?? zoneRows.length, tr('history.zoneHistory')],
|
||
[tr('history.haSamples'), app.historyCounts.ha ?? sensorRows.length, tr('history.haHistory')],
|
||
];
|
||
host.innerHTML = cards.map(([label, value, detail]) => `<div class="history-stat"><small>${esc(label)}</small><strong>${esc(value)}</strong><span>${esc(detail)}</span></div>`).join('');
|
||
}
|
||
|
||
function renderOverviewHistory() {
|
||
const host = $('#historyCharts');
|
||
host.innerHTML = historyChartMarkup('overviewIndoorChart', tr('history.allGreeIndoor'), tr('history.allGreeIndoorHint'))
|
||
+ historyChartMarkup('overviewOutdoorChart', tr('history.allOutdoor'), tr('history.allOutdoorHint'))
|
||
+ historyChartMarkup('overviewZonesChart', tr('history.allZoneControl'), tr('history.allZoneControlHint'));
|
||
|
||
const deviceRows = app.historyData.devices;
|
||
const indoorSeries = app.devices.map((device, index) => ({ label: device.name, color: historySeriesColor(index), value: row => !row.zone_id && !row.entity_id && row.device_id === device.id ? historyNumber(row.indoor_temperature) : NaN }));
|
||
drawLineChart($('#overviewIndoorChart'), indoorSeries, deviceRows, { height: 360 }); renderLegend($('#overviewIndoorChartLegend'), indoorSeries);
|
||
|
||
const outdoorDeviceSeries = app.devices.filter(device => deviceRows.some(row => row.device_id === device.id && Number.isFinite(historyNumber(row.outdoor_temperature)))).map((device, index) => ({ label: `${device.name} · ${tr('history.greeOutdoor')}`, color: historySeriesColor(index), value: row => !row.zone_id && !row.entity_id && row.device_id === device.id ? historyNumber(row.outdoor_temperature) : NaN }));
|
||
const outdoorEntities = [...new Set(app.historyData.sensors.filter(row => row.kind === 'outdoor').map(row => row.entity_id))];
|
||
const outdoorHaSeries = outdoorEntities.map((entity, index) => ({ label: `HA · ${haSensorLabel(entity)}`, color: historySeriesColor(index + outdoorDeviceSeries.length), dash: [6, 4], value: row => row.entity_id === entity ? historyNumber(row.temperature) : NaN }));
|
||
const outdoorRows = [...deviceRows, ...app.historyData.sensors.filter(row => row.kind === 'outdoor')];
|
||
const outdoorSeries = [...outdoorDeviceSeries, ...outdoorHaSeries];
|
||
drawLineChart($('#overviewOutdoorChart'), outdoorSeries, outdoorRows, { height: 320 }); renderLegend($('#overviewOutdoorChartLegend'), outdoorSeries);
|
||
|
||
const zoneRows = app.historyData.zones;
|
||
const zoneSeries = app.zones.map((zone, index) => ({ label: zone.name, color: historySeriesColor(index), value: row => row.zone_id === zone.id ? historyNumber(row.control_temperature) : NaN }));
|
||
drawLineChart($('#overviewZonesChart'), zoneSeries, zoneRows, { height: 340 }); renderLegend($('#overviewZonesChartLegend'), zoneSeries);
|
||
}
|
||
|
||
function renderZoneHistory() {
|
||
const selected = app.historyZone;
|
||
const rows = selected === 'all' ? app.historyData.zones : app.historyData.zones.filter(row => row.zone_id === selected);
|
||
const host = $('#historyCharts');
|
||
host.innerHTML = historyChartMarkup('zoneTemperatureChart', tr('history.temperatureOverview'), tr('history.temperatureOverviewHint')) + historyChartMarkup('zoneOperationChart', tr('history.operationOverview'), tr('history.operationOverviewHint'), true);
|
||
if (selected === 'all') {
|
||
const series = app.zones.map((zone, index) => ({ label: zone.name, color: historySeriesColor(index), value: row => row.zone_id === zone.id ? historyNumber(row.control_temperature) : NaN }));
|
||
drawLineChart($('#zoneTemperatureChart'), series, rows, { height: 360 }); renderLegend($('#zoneTemperatureChartLegend'), series);
|
||
const targets = app.zones.map((zone, index) => ({ label: `${zone.name} · ${tr('history.targetShort')}`, color: historySeriesColor(index), dash: [5, 4], value: row => row.zone_id === zone.id ? historyNumber(row.target_temperature) : NaN }));
|
||
drawLineChart($('#zoneOperationChart'), targets, rows, { height: 260 }); renderLegend($('#zoneOperationChartLegend'), targets);
|
||
return;
|
||
}
|
||
const temperatureSeries = [
|
||
{ label: tr('history.greeSensor'), color: cssColor('--accent', '#3ecf8e'), value: row => historyNumber(row.gree_temperature) },
|
||
{ label: tr('history.roomSensor'), color: cssColor('--info', '#60a5fa'), value: row => historyNumber(row.external_temperature) },
|
||
{ label: tr('history.controlTemperature'), color: cssColor('--teal', '#2dd4bf'), width: 2.8, value: row => historyNumber(row.control_temperature) },
|
||
{ label: tr('history.comfortTarget'), color: cssColor('--warning', '#f59e0b'), dash: [7, 5], value: row => historyNumber(row.target_temperature) },
|
||
{ label: tr('history.deviceSetpoint'), color: cssColor('--purple', '#a78bfa'), dash: [3, 4], value: row => historyNumber(row.device_setpoint) },
|
||
{ label: tr('history.outdoorTemperature'), color: cssColor('--muted-strong', '#9ca3af'), dash: [2, 5], value: row => historyNumber(row.outdoor_temperature) },
|
||
];
|
||
drawLineChart($('#zoneTemperatureChart'), temperatureSeries, rows, { height: 360 }); renderLegend($('#zoneTemperatureChartLegend'), temperatureSeries);
|
||
const operationSeries = [
|
||
{ label: tr('history.fanSpeed'), color: cssColor('--info', '#60a5fa'), step: true, value: row => historyNumber(row.fan_speed), tooltipValue: value => fanLabel(Math.round(value)) },
|
||
{ label: tr('history.demand'), color: cssColor('--accent', '#3ecf8e'), step: true, width: 2.4, value: row => row.demand ? 4.5 : 0.5, tooltipValue: (_, row) => row.demand ? tr('common.active') : tr('common.disabled') },
|
||
{ label: tr('history.power'), color: cssColor('--warning', '#f59e0b'), step: true, dash: [5, 4], value: row => row.power ? 3.5 : 0.5, tooltipValue: (_, row) => row.power ? tr('common.on') : tr('common.off') },
|
||
];
|
||
drawLineChart($('#zoneOperationChart'), operationSeries, rows, { height: 260, minValue: 0, maxValue: 5, binaryLabels: true }); renderLegend($('#zoneOperationChartLegend'), operationSeries);
|
||
}
|
||
|
||
function renderDeviceHistory() {
|
||
const selected = app.historyDevice;
|
||
const rows = selected === 'all' ? app.historyData.devices : app.historyData.devices.filter(row => row.device_id === selected);
|
||
const host = $('#historyCharts');
|
||
host.innerHTML = historyChartMarkup('deviceIndoorChart', tr('history.deviceIndoor'), tr('history.deviceIndoorHint')) + historyChartMarkup('deviceOutdoorChart', tr('history.deviceOutdoor'), tr('history.deviceOutdoorHint')) + historyChartMarkup('deviceTargetChart', tr('history.deviceTargets'), tr('history.deviceTargetsHint'), true);
|
||
const devices = selected === 'all' ? app.devices : app.devices.filter(device => device.id === selected);
|
||
const indoor = devices.map((device, index) => ({ label: device.name, color: historySeriesColor(index), value: row => row.device_id === device.id ? historyNumber(row.indoor_temperature) : NaN }));
|
||
const outdoor = devices.filter(device => rows.some(row => row.device_id === device.id && Number.isFinite(historyNumber(row.outdoor_temperature)))).map((device, index) => ({ label: device.name, color: historySeriesColor(index), value: row => row.device_id === device.id ? historyNumber(row.outdoor_temperature) : NaN }));
|
||
const targets = devices.map((device, index) => ({ label: device.name, color: historySeriesColor(index), dash: [6, 4], value: row => row.device_id === device.id ? historyNumber(row.target_temperature) : NaN }));
|
||
drawLineChart($('#deviceIndoorChart'), indoor, rows, { height: 350 }); renderLegend($('#deviceIndoorChartLegend'), indoor);
|
||
drawLineChart($('#deviceOutdoorChart'), outdoor, rows, { height: 310 }); renderLegend($('#deviceOutdoorChartLegend'), outdoor);
|
||
drawLineChart($('#deviceTargetChart'), targets, rows, { height: 260 }); renderLegend($('#deviceTargetChartLegend'), targets);
|
||
}
|
||
|
||
function renderSensorHistory() {
|
||
const selected = app.historySensor;
|
||
const rows = selected === 'all' ? app.historyData.sensors : app.historyData.sensors.filter(row => row.entity_id === selected);
|
||
const entities = [...new Set(rows.map(row => row.entity_id))];
|
||
const host = $('#historyCharts');
|
||
host.innerHTML = historyChartMarkup('haSensorsChart', tr('history.haSensors'), tr('history.haSensorsHint'));
|
||
const series = entities.map((entity, index) => ({ label: haSensorLabel(entity), color: historySeriesColor(index), dash: rows.some(row => row.entity_id === entity && row.kind === 'outdoor') ? [6, 4] : [], value: row => row.entity_id === entity ? historyNumber(row.temperature) : NaN }));
|
||
drawLineChart($('#haSensorsChart'), series, rows, { height: 370 }); renderLegend($('#haSensorsChartLegend'), series);
|
||
}
|
||
|
||
function customSeriesOptions() {
|
||
const items = [];
|
||
app.devices.forEach(device => {
|
||
items.push([`device|${device.id}|indoor`, `${device.name} · ${tr('history.indoorTemperature')}`]);
|
||
items.push([`device|${device.id}|outdoor`, `${device.name} · ${tr('history.greeOutdoor')}`]);
|
||
items.push([`device|${device.id}|target`, `${device.name} · ${tr('history.deviceTarget')}`]);
|
||
});
|
||
app.zones.forEach(zone => {
|
||
items.push([`zone|${zone.id}|control`, `${zone.name} · ${tr('history.controlTemperature')}`]);
|
||
items.push([`zone|${zone.id}|gree`, `${zone.name} · ${tr('history.greeSensor')}`]);
|
||
items.push([`zone|${zone.id}|external`, `${zone.name} · ${tr('history.roomSensor')}`]);
|
||
items.push([`zone|${zone.id}|target`, `${zone.name} · ${tr('history.comfortTarget')}`]);
|
||
items.push([`zone|${zone.id}|device_target`, `${zone.name} · ${tr('history.deviceSetpoint')}`]);
|
||
items.push([`zone|${zone.id}|outdoor`, `${zone.name} · ${tr('history.outdoorTemperature')}`]);
|
||
});
|
||
[...new Set(app.historyData.sensors.map(row => row.entity_id))].forEach(entity => items.push([`ha|${entity}|temperature`, `HA · ${haSensorLabel(entity)}`]));
|
||
return items;
|
||
}
|
||
|
||
function customSeriesDefinition(key, index = 0) {
|
||
const [kind, id, field] = String(key).split('|');
|
||
const color = historySeriesColor(index);
|
||
if (kind === 'device') {
|
||
const device = app.devices.find(item => item.id === id); if (!device) return null;
|
||
const labels = { indoor: tr('history.indoorTemperature'), outdoor: tr('history.greeOutdoor'), target: tr('history.deviceTarget') };
|
||
const fields = { indoor: 'indoor_temperature', outdoor: 'outdoor_temperature', target: 'target_temperature' };
|
||
return { key, label: `${device.name} · ${labels[field] || field}`, color, dash: field === 'target' ? [6, 4] : [], value: row => !row.zone_id && !row.entity_id && row.device_id === id ? historyNumber(row[fields[field]]) : NaN };
|
||
}
|
||
if (kind === 'zone') {
|
||
const zone = app.zones.find(item => item.id === id); if (!zone) return null;
|
||
const fields = { control: ['control_temperature', tr('history.controlTemperature')], gree: ['gree_temperature', tr('history.greeSensor')], external: ['external_temperature', tr('history.roomSensor')], target: ['target_temperature', tr('history.comfortTarget')], device_target: ['device_setpoint', tr('history.deviceSetpoint')], outdoor: ['outdoor_temperature', tr('history.outdoorTemperature')] };
|
||
const info = fields[field]; if (!info) return null;
|
||
return { key, label: `${zone.name} · ${info[1]}`, color, dash: ['target', 'device_target', 'outdoor'].includes(field) ? [6, 4] : [], value: row => row.zone_id === id ? historyNumber(row[info[0]]) : NaN };
|
||
}
|
||
if (kind === 'ha') return { key, label: `HA · ${haSensorLabel(id)}`, color, dash: [3, 4], value: row => row.entity_id === id ? historyNumber(row.temperature) : NaN };
|
||
return null;
|
||
}
|
||
|
||
function persistSavedCharts() {
|
||
localStorage.setItem('gree_controller_saved_charts', JSON.stringify(app.savedCharts.slice(0, 30)));
|
||
}
|
||
|
||
function renderCustomBuilder() {
|
||
const host = $('#historyCustomBuilder'); if (!host) return;
|
||
if (app.historyTab !== 'custom') { host.innerHTML = ''; host.classList.remove('active'); return; }
|
||
host.classList.add('active');
|
||
const options = customSeriesOptions();
|
||
const selected = app.customChartSeries.map((key, index) => customSeriesDefinition(key, index)).filter(Boolean);
|
||
host.innerHTML = `<div class="panel custom-chart-panel"><div class="chart-title-row"><div><h3>${esc(tr('history.customTitle'))}</h3><p>${esc(tr('history.customDescription'))}</p></div></div>
|
||
<div class="custom-chart-add"><select id="customSeriesSelect">${options.map(([value, label]) => `<option value="${esc(value)}">${esc(label)}</option>`).join('')}</select><button class="secondary" data-history-action="add-series">${esc(tr('history.addSeries'))}</button></div>
|
||
<div class="custom-series-list">${selected.length ? selected.map((item, index) => `<span class="custom-series-chip"><i style="--chip-color:${esc(item.color)}"></i>${esc(item.label)}<button data-history-action="remove-series" data-index="${index}" aria-label="${esc(tr('actions.remove'))}">×</button></span>`).join('') : `<span class="field-note">${esc(tr('history.noCustomSeries'))}</span>`}</div>
|
||
<div class="custom-chart-save"><input id="customChartName" placeholder="${esc(tr('history.chartName'))}"><button class="secondary" data-history-action="save-chart">${esc(tr('actions.save'))}</button><button class="primary" data-history-action="copy-chart-link">${esc(tr('history.copyLink'))}</button></div>
|
||
<div class="saved-chart-list">${app.savedCharts.length ? app.savedCharts.map(item => `<div class="saved-chart-row"><button data-history-action="load-chart" data-id="${esc(item.id)}"><strong>${esc(item.name)}</strong><small>${esc(item.series.length)} ${esc(tr('history.series'))}</small></button><button class="danger" data-history-action="delete-chart" data-id="${esc(item.id)}">×</button></div>`).join('') : `<span class="field-note">${esc(tr('history.noSavedCharts'))}</span>`}</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderCustomHistory() {
|
||
renderCustomBuilder();
|
||
const host = $('#historyCharts');
|
||
host.innerHTML = historyChartMarkup('customHistoryChart', tr('history.customChart'), tr('history.customChartHint'));
|
||
const series = app.customChartSeries.map((key, index) => customSeriesDefinition(key, index)).filter(Boolean);
|
||
const rows = [...app.historyData.devices, ...app.historyData.zones, ...app.historyData.sensors];
|
||
drawLineChart($('#customHistoryChart'), series, rows, { height: 390 }); renderLegend($('#customHistoryChartLegend'), series);
|
||
}
|
||
|
||
function renderHistoryPage() {
|
||
renderHistorySummary();
|
||
renderCustomBuilder();
|
||
if (app.historyTab === 'overview') renderOverviewHistory();
|
||
else if (app.historyTab === 'zones') renderZoneHistory();
|
||
else if (app.historyTab === 'devices') renderDeviceHistory();
|
||
else if (app.historyTab === 'energy') renderEnergyHistory();
|
||
else if (app.historyTab === 'sensors') renderSensorHistory();
|
||
else renderCustomHistory();
|
||
}
|
||
|
||
function drawCurrentChartIfVisible() {
|
||
if (app.currentView === 'history') renderHistoryPage();
|
||
}
|
||
|
||
async function handleHistoryAction(button) {
|
||
const action = button.dataset.historyAction;
|
||
if (action === 'add-series') {
|
||
const value = $('#customSeriesSelect')?.value;
|
||
if (value && !app.customChartSeries.includes(value)) app.customChartSeries.push(value);
|
||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath(), true); return;
|
||
}
|
||
if (action === 'remove-series') {
|
||
app.customChartSeries.splice(Number(button.dataset.index), 1);
|
||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath(), true); return;
|
||
}
|
||
if (action === 'save-chart') {
|
||
if (!app.customChartSeries.length) return toast(tr('history.noCustomSeries'), true);
|
||
const name = $('#customChartName')?.value.trim() || tr('history.customChart');
|
||
const item = { id: `chart-${Date.now()}`, name, series: [...app.customChartSeries], hours: $('#historyHours')?.value || '24' };
|
||
app.savedCharts.unshift(item); persistSavedCharts(); renderCustomHistory(); toast(tr('history.chartSaved')); return;
|
||
}
|
||
if (action === 'load-chart') {
|
||
const item = app.savedCharts.find(entry => entry.id === button.dataset.id); if (!item) return;
|
||
app.customChartSeries = [...item.series]; if ($('#historyHours') && item.hours) $('#historyHours').value = String(item.hours);
|
||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath()); return;
|
||
}
|
||
if (action === 'delete-chart') {
|
||
app.savedCharts = app.savedCharts.filter(entry => entry.id !== button.dataset.id); persistSavedCharts(); renderCustomHistory(); return;
|
||
}
|
||
if (action === 'copy-chart-link') {
|
||
if (!app.customChartSeries.length) return toast(tr('history.noCustomSeries'), true);
|
||
const path = currentHistoryPath(); updateBrowserUrl(path, true); const link = `${location.origin}${APP_BASE}${path}`;
|
||
try { await navigator.clipboard.writeText(link); } catch (_) { const area = document.createElement('textarea'); area.value = link; document.body.appendChild(area); area.select(); document.execCommand('copy'); area.remove(); }
|
||
toast(tr('history.linkCopied')); return;
|
||
}
|
||
}
|
||
|
||
|
||
async function loadEnergyHistory() {
|
||
const energyDevices = app.devices.filter(device => device.capabilities?.energy_meter === true || !!device.ha_energy_entity_id);
|
||
if (!energyDevices.length) {
|
||
app.historyEnergy = null;
|
||
return;
|
||
}
|
||
if (!energyDevices.some(device => device.id === app.historyEnergyDevice)) app.historyEnergyDevice = energyDevices[0].id;
|
||
const hours = Number($('#historyHours')?.value || 24);
|
||
const days = Math.max(1, Math.ceil(hours / 24));
|
||
app.historyEnergy = await api(`/api/history/energy?device_id=${encodeURIComponent(app.historyEnergyDevice)}&interval=${encodeURIComponent(app.historyEnergyInterval)}&days=${days}&limit=100000`);
|
||
}
|
||
|
||
function renderEnergyHistorySummary() {
|
||
const host = $('#historySummary'); if (!host) return;
|
||
const data = app.historyEnergy;
|
||
if (!data || data.source === 'none') { host.innerHTML = ''; return; }
|
||
const summary = data.summary || {};
|
||
const rows = [
|
||
[tr('energy.today'), summary.today],
|
||
[tr('energy.yesterday'), summary.yesterday],
|
||
[tr('energy.currentMonth'), summary.current_month],
|
||
[tr('energy.previousMonth'), summary.previous_month],
|
||
[tr('energy.periodTotal'), summary.period_total],
|
||
];
|
||
host.innerHTML = rows.map(([label, value]) => `<div class="history-stat"><small>${esc(label)}</small><strong>${Number(value || 0).toLocaleString(locale(), { maximumFractionDigits: 3 })} kWh</strong><span>${esc(data.source === 'gree_cloud' ? 'GREE Cloud' : 'Home Assistant')}</span></div>`).join('');
|
||
}
|
||
|
||
function renderEnergyHistory() {
|
||
const host = $('#historyCharts'); if (!host) return;
|
||
const data = app.historyEnergy;
|
||
renderEnergyHistorySummary();
|
||
if (!data || data.source === 'none') {
|
||
host.innerHTML = `<div class="empty"><strong>${esc(tr('energy.noData'))}</strong></div>`;
|
||
return;
|
||
}
|
||
host.innerHTML = historyChartMarkup('energyConsumptionChart', tr('energy.title'), tr('energy.chartHint'));
|
||
drawEnergyBarChart($('#energyConsumptionChart'), data.buckets || [], { height: 360 });
|
||
}
|