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])=>`
${esc(label)}${esc(value)}${esc(detail)}
`).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=`

${esc(tr('history.customTitle'))}

${esc(tr('history.customDescription'))}

${selected.length?selected.map((item,index)=>`${esc(item.label)}`).join(''):`${esc(tr('history.noCustomSeries'))}`}
${app.savedCharts.length?app.savedCharts.map(item=>`
`).join(''):`${esc(tr('history.noSavedCharts'))}`}
`; } 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; } }