199 lines
16 KiB
JavaScript
199 lines
16 KiB
JavaScript
function renderHistorySummary() {
|
||
const host=$('#historySummary'); if(!host) 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==='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;
|
||
}
|
||
}
|
||
|