index.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>MPPT Solar Monitor - DM Series</title>
  7. <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
  8. <style>
  9. *{margin:0;padding:0;box-sizing:border-box}
  10. body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh}
  11. .header{background:linear-gradient(135deg,#1e293b,#334155);padding:16px 24px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #475569}
  12. .header h1{font-size:20px;color:#38bdf8}
  13. .header .status{display:flex;gap:16px;align-items:center;font-size:13px}
  14. .header .status .dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px}
  15. .dot-green{background:#22c55e}.dot-red{background:#ef4444}.dot-yellow{background:#eab308}
  16. .tabs{display:flex;background:#1e293b;border-bottom:1px solid #334155}
  17. .tab{padding:12px 24px;cursor:pointer;font-size:14px;color:#94a3b8;border-bottom:2px solid transparent;transition:all .2s}
  18. .tab:hover{color:#e2e8f0}.tab.active{color:#38bdf8;border-bottom-color:#38bdf8}
  19. .page{display:none;padding:20px 24px}.page.active{display:block}
  20. /* Device Cards */
  21. .device-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:16px;margin-bottom:24px}
  22. .device-card{background:#1e293b;border-radius:12px;border:1px solid #334155;overflow:hidden}
  23. .device-card.offline{opacity:.6;border-color:#7f1d1d}
  24. .card-header{padding:12px 16px;background:#334155;display:flex;justify-content:space-between;align-items:center}
  25. .card-header h3{font-size:15px;color:#f1f5f9}
  26. .card-header .badge{padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}
  27. .badge-online{background:#065f46;color:#6ee7b7}.badge-offline{background:#7f1d1d;color:#fca5a5}
  28. .card-body{padding:16px}
  29. .data-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
  30. .data-item{display:flex;justify-content:space-between;padding:6px 10px;background:#0f172a;border-radius:6px;font-size:13px}
  31. .data-item .label{color:#94a3b8}.data-item .value{color:#f1f5f9;font-weight:500}
  32. .section-title{font-size:13px;color:#38bdf8;margin:12px 0 8px;padding-bottom:4px;border-bottom:1px solid #334155}
  33. /* History Page */
  34. .history-controls{background:#1e293b;border-radius:12px;padding:16px;margin-bottom:20px;border:1px solid #334155}
  35. .controls-row{display:flex;flex-wrap:wrap;gap:12px;align-items:end}
  36. .control-group{display:flex;flex-direction:column;gap:4px}
  37. .control-group label{font-size:12px;color:#94a3b8}
  38. .control-group input,.control-group select{background:#0f172a;border:1px solid #475569;color:#e2e8f0;padding:8px 12px;border-radius:6px;font-size:13px}
  39. .btn{padding:8px 16px;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;transition:all .2s}
  40. .btn-primary{background:#0ea5e9;color:#fff}.btn-primary:hover{background:#0284c7}
  41. .btn-secondary{background:#334155;color:#e2e8f0}.btn-secondary:hover{background:#475569}
  42. .quick-btns{display:flex;gap:6px}
  43. .quick-btns .btn{padding:6px 10px;font-size:12px}
  44. /* Chart */
  45. .chart-container{background:#1e293b;border-radius:12px;padding:16px;margin-bottom:20px;border:1px solid #334155}
  46. .chart-wrapper{position:relative;height:300px}
  47. /* Table */
  48. .table-container{background:#1e293b;border-radius:12px;border:1px solid #334155;overflow:hidden}
  49. table{width:100%;border-collapse:collapse;font-size:13px}
  50. th{background:#334155;color:#94a3b8;padding:10px 12px;text-align:left;font-weight:500;white-space:nowrap}
  51. td{padding:8px 12px;border-top:1px solid #1e293b;white-space:nowrap}
  52. tr:hover td{background:#1e293b}
  53. .fault-normal{color:#22c55e}.fault-warning{color:#ef4444}
  54. /* Pagination */
  55. .pagination{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#334155}
  56. .pagination .info{font-size:13px;color:#94a3b8}
  57. .pagination .btns{display:flex;gap:8px}
  58. /* Responsive */
  59. @media(max-width:768px){.device-grid{grid-template-columns:1fr}.controls-row{flex-direction:column}.data-grid{grid-template-columns:1fr}}
  60. </style>
  61. </head>
  62. <body>
  63. <div class="header">
  64. <h1>MPPT Solar Monitor - DM Series</h1>
  65. <div class="status">
  66. <span><span class="dot dot-green" id="conn-dot"></span> <span id="conn-status">Connecting...</span></span>
  67. <span>Devices: <strong id="dev-online">0</strong>/<strong id="dev-total">0</strong></span>
  68. <span>Records: <strong id="db-records">0</strong></span>
  69. </div>
  70. </div>
  71. <div class="tabs">
  72. <div class="tab active" onclick="switchPage('realtime')">Real-time Monitor</div>
  73. <div class="tab" onclick="switchPage('history')">History Query</div>
  74. <div class="tab" onclick="switchPage('settings')">Device Settings</div>
  75. </div>
  76. <!-- Real-time Page -->
  77. <div class="page active" id="page-realtime">
  78. <div class="device-grid" id="device-grid"></div>
  79. </div>
  80. <!-- History Page -->
  81. <div class="page" id="page-history">
  82. <div class="history-controls">
  83. <div class="controls-row">
  84. <div class="control-group">
  85. <label>Device</label>
  86. <select id="hist-device"><option value="0">All Devices</option></select>
  87. </div>
  88. <div class="control-group">
  89. <label>Start Time</label>
  90. <input type="datetime-local" id="hist-start">
  91. </div>
  92. <div class="control-group">
  93. <label>End Time</label>
  94. <input type="datetime-local" id="hist-end">
  95. </div>
  96. <div class="control-group">
  97. <label>Page Size</label>
  98. <select id="hist-size">
  99. <option value="20">20</option><option value="50">50</option>
  100. <option value="100">100</option><option value="200">200</option>
  101. </select>
  102. </div>
  103. <div class="control-group">
  104. <label>Sort</label>
  105. <select id="hist-sort"><option value="desc">Newest First</option><option value="asc">Oldest First</option></select>
  106. </div>
  107. <div class="control-group">
  108. <label>&nbsp;</label>
  109. <button class="btn btn-primary" onclick="loadHistory(1)">Query</button>
  110. </div>
  111. <div class="control-group">
  112. <label>Quick Range</label>
  113. <div class="quick-btns">
  114. <button class="btn btn-secondary" onclick="setQuickRange(1)">1h</button>
  115. <button class="btn btn-secondary" onclick="setQuickRange(24)">24h</button>
  116. <button class="btn btn-secondary" onclick="setQuickRange(168)">7d</button>
  117. <button class="btn btn-secondary" onclick="setQuickRange(720)">30d</button>
  118. </div>
  119. </div>
  120. </div>
  121. </div>
  122. <div class="chart-container">
  123. <div class="chart-wrapper"><canvas id="history-chart"></canvas></div>
  124. </div>
  125. <div class="table-container">
  126. <table><thead><tr>
  127. <th>Time</th><th>Dev</th><th>Batt V</th><th>SOC</th><th>PV V</th><th>PV A</th>
  128. <th>Power</th><th>Temp</th><th>Fault</th><th>Mode</th><th>Batt Type</th><th>Energy</th>
  129. </tr></thead><tbody id="history-tbody"></tbody></table>
  130. <div class="pagination">
  131. <div class="info" id="hist-info">No data</div>
  132. <div class="btns">
  133. <button class="btn btn-secondary" onclick="loadHistory(histPage-1)" id="btn-prev" disabled>Prev</button>
  134. <button class="btn btn-secondary" onclick="loadHistory(histPage+1)" id="btn-next" disabled>Next</button>
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. <!-- Settings Page -->
  140. <div class="page" id="page-settings">
  141. <div class="history-controls">
  142. <h3 style="color:#38bdf8;margin-bottom:12px;font-size:16px">Device Configuration (Output Registers)</h3>
  143. <p style="color:#94a3b8;font-size:13px;margin-bottom:16px">Write parameters to MPPT device via Modbus function code 0x10. Changes take effect immediately.</p>
  144. <div class="controls-row">
  145. <div class="control-group">
  146. <label>Target Device</label>
  147. <select id="set-device"></select>
  148. </div>
  149. <div class="control-group">
  150. <label>Parameter</label>
  151. <select id="set-param" onchange="updateParamInfo()">
  152. <option value="battery_type">Battery Type (电池类型)</option>
  153. <option value="battery_level">Battery Level (电池等级)</option>
  154. <option value="charge_current_limit">Charge Current Limit (充电电流限制 %)</option>
  155. <option value="full_charge_voltage">Full Charge Voltage (充满电压, 0.1V)</option>
  156. <option value="device_address">485 Address (设备地址)</option>
  157. <option value="charge_mode">Charge Mode (充电模式)</option>
  158. </select>
  159. </div>
  160. <div class="control-group">
  161. <label>Value</label>
  162. <input type="number" id="set-value" min="0" max="255" step="1">
  163. </div>
  164. <div class="control-group">
  165. <label>&nbsp;</label>
  166. <button class="btn btn-primary" onclick="writeSetting()" id="btn-write">Write</button>
  167. </div>
  168. </div>
  169. <div id="param-info" style="margin-top:12px;padding:10px;background:#0f172a;border-radius:6px;font-size:12px;color:#94a3b8"></div>
  170. <div id="write-result" style="margin-top:12px;padding:10px;border-radius:6px;display:none"></div>
  171. </div>
  172. <div class="table-container" style="margin-top:20px">
  173. <table>
  174. <thead><tr><th>Register</th><th>Address</th><th>Range</th><th>Description</th><th>Access</th></tr></thead>
  175. <tbody>
  176. <tr><td>Battery Type</td><td>40001 (0x0000)</td><td>0-5</td><td>0=胶体 1=密封 2=磷酸铁锂 3=三元锂电 4=自定义 5=加水</td><td style="color:#22c55e">R/W</td></tr>
  177. <tr><td>Battery Level</td><td>40002 (0x0001)</td><td>0-6</td><td>0=自动识别 1=12V 2=24V 3=36V 4=48V 5=60V 6=72V</td><td style="color:#22c55e">R/W</td></tr>
  178. <tr><td>Charge Current Limit</td><td>40003 (0x0002)</td><td>5-100</td><td>PV充电电流限制 (%)</td><td style="color:#22c55e">R/W</td></tr>
  179. <tr><td>Over-discharge Voltage</td><td>40004 (0x0003)</td><td>80-600</td><td>电池过放电压 (0.1V)</td><td style="color:#ef4444">Read Only</td></tr>
  180. <tr><td>Boost Voltage</td><td>40005 (0x0004)</td><td>80-600</td><td>电池提升电压 (0.1V)</td><td style="color:#ef4444">Read Only</td></tr>
  181. <tr><td>Full Charge Voltage</td><td>40006 (0x0005)</td><td>80-600</td><td>电池充满电压 (0.1V) - 仅自定义电池可写</td><td style="color:#eab308">R/W (Custom only)</td></tr>
  182. <tr><td>Recovery Voltage</td><td>40007 (0x0006)</td><td>80-600</td><td>电池过放恢复电压 (0.1V)</td><td style="color:#ef4444">Read Only</td></tr>
  183. <tr><td>485 Address</td><td>40008 (0x0007)</td><td>0-247</td><td>设备485通信地址</td><td style="color:#22c55e">R/W</td></tr>
  184. <tr><td>Charge Mode</td><td>40009 (0x0008)</td><td>0-1</td><td>0=MPPT 1=DCDC</td><td style="color:#22c55e">R/W</td></tr>
  185. </tbody>
  186. </table>
  187. </div>
  188. </div>
  189. <script>
  190. let histPage = 1, histTotalPages = 0, historyChart = null;
  191. const faultDescs = {20:'Normal',30:'PV Overvoltage',31:'PV Overcurrent',32:'Battery Low',33:'Battery Overvoltage',38:'Temp High',50:'Temp Sensor Fault',88:'Not Decrypted'};
  192. function switchPage(name) {
  193. document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  194. document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  195. document.getElementById('page-' + name).classList.add('active');
  196. const tabIdx = name === 'realtime' ? 0 : name === 'history' ? 1 : 2;
  197. document.querySelectorAll('.tab')[tabIdx].classList.add('active');
  198. if (name === 'settings') updateSettingsDeviceList();
  199. }
  200. function fmtTime(ts) {
  201. if (!ts) return '-';
  202. return new Date(ts * 1000).toLocaleString('zh-CN', {hour12: false});
  203. }
  204. function renderDevices(data) {
  205. const grid = document.getElementById('device-grid');
  206. let onlineCount = 0;
  207. let html = '';
  208. data.devices.forEach(dev => {
  209. if (dev.online) onlineCount++;
  210. const inp = dev.input_registers || {};
  211. const out = dev.output_registers || {};
  212. html += `<div class="device-card ${dev.online ? '' : 'offline'}">
  213. <div class="card-header">
  214. <h3>Device #${dev.device_addr} ${dev.online ? '' : '(OFFLINE)'}</h3>
  215. <span class="badge ${dev.online ? 'badge-online' : 'badge-offline'}">${dev.online ? 'ONLINE' : 'OFFLINE'}</span>
  216. </div>
  217. <div class="card-body">
  218. <div class="section-title">Real-time Data (Input Registers)</div>
  219. <div class="data-grid">
  220. <div class="data-item"><span class="label">Battery Voltage</span><span class="value">${(inp.battery_voltage||0).toFixed(1)} V</span></div>
  221. <div class="data-item"><span class="label">Battery SOC</span><span class="value">${inp.battery_soc||0} %</span></div>
  222. <div class="data-item"><span class="label">PV Voltage</span><span class="value">${(inp.pv_voltage||0).toFixed(1)} V</span></div>
  223. <div class="data-item"><span class="label">PV Current</span><span class="value">${(inp.pv_charge_current||0).toFixed(1)} A</span></div>
  224. <div class="data-item"><span class="label">PV Power</span><span class="value">${(inp.pv_input_power||0).toFixed(1)} W</span></div>
  225. <div class="data-item"><span class="label">Temperature</span><span class="value">${inp.machine_temp >= 0 ? inp.machine_temp + ' °C' : 'Fault'}</span></div>
  226. <div class="data-item"><span class="label">Fault</span><span class="value ${inp.fault_code===20?'fault-normal':'fault-warning'}">${inp.fault_desc||'Unknown'}</span></div>
  227. <div class="data-item"><span class="label">Charge State</span><span class="value">${inp.charge_state_desc||'-'}</span></div>
  228. <div class="data-item"><span class="label">Cumulative Energy</span><span class="value">${(inp.cumulative_energy||0).toFixed(1)} kWh</span></div>
  229. <div class="data-item"><span class="label">Software Ver</span><span class="value">v${inp.software_version||0}</span></div>
  230. </div>
  231. <div class="section-title">Configuration (Output Registers)</div>
  232. <div class="data-grid">
  233. <div class="data-item"><span class="label">Battery Type</span><span class="value">${out.battery_type_desc||'-'}</span></div>
  234. <div class="data-item"><span class="label">Battery Level</span><span class="value">${out.battery_level_desc||'-'}</span></div>
  235. <div class="data-item"><span class="label">Charge Mode</span><span class="value">${out.charge_mode_desc||'-'}</span></div>
  236. <div class="data-item"><span class="label">Current Limit</span><span class="value">${out.pv_charge_current_limit||0} %</span></div>
  237. <div class="data-item"><span class="label">Full Voltage</span><span class="value">${(out.batt_full_voltage||0).toFixed(1)} V</span></div>
  238. <div class="data-item"><span class="label">Boost Voltage</span><span class="value">${(out.batt_boost_voltage||0).toFixed(1)} V</span></div>
  239. <div class="data-item"><span class="label">Over-discharge V</span><span class="value">${(out.batt_over_discharge_v||0).toFixed(1)} V</span></div>
  240. <div class="data-item"><span class="label">Recovery V</span><span class="value">${(out.batt_recovery_voltage||0).toFixed(1)} V</span></div>
  241. <div class="data-item"><span class="label">485 Address</span><span class="value">${out.device_485_addr||'-'}</span></div>
  242. <div class="data-item"><span class="label">Device Type</span><span class="value">${inp.device_type||'-'}</span></div>
  243. </div>
  244. <div style="margin-top:8px;font-size:11px;color:#64748b">Last update: ${fmtTime(dev.last_update)}</div>
  245. </div>
  246. </div>`;
  247. });
  248. grid.innerHTML = html;
  249. document.getElementById('dev-online').textContent = onlineCount;
  250. document.getElementById('dev-total').textContent = data.device_count;
  251. }
  252. async function fetchRealtime() {
  253. try {
  254. const res = await fetch('/api/data');
  255. const data = await res.json();
  256. renderDevices(data);
  257. document.getElementById('conn-dot').className = 'dot dot-green';
  258. document.getElementById('conn-status').textContent = 'Connected';
  259. // Update device selector in history page
  260. const sel = document.getElementById('hist-device');
  261. if (sel.options.length <= 1) {
  262. data.devices.forEach(d => {
  263. const opt = document.createElement('option');
  264. opt.value = d.device_addr;
  265. opt.textContent = 'Device #' + d.device_addr;
  266. sel.appendChild(opt);
  267. });
  268. }
  269. } catch(e) {
  270. document.getElementById('conn-dot').className = 'dot dot-red';
  271. document.getElementById('conn-status').textContent = 'Disconnected';
  272. }
  273. }
  274. async function fetchStatus() {
  275. try {
  276. const res = await fetch('/api/status');
  277. const data = await res.json();
  278. document.getElementById('db-records').textContent = data.db_records || 0;
  279. } catch(e) {}
  280. }
  281. function setQuickRange(hours) {
  282. const now = new Date();
  283. const start = new Date(now.getTime() - hours * 3600000);
  284. document.getElementById('hist-start').value = start.toISOString().slice(0, 16);
  285. document.getElementById('hist-end').value = now.toISOString().slice(0, 16);
  286. loadHistory(1);
  287. }
  288. async function loadHistory(page) {
  289. if (page < 1) return;
  290. if (histTotalPages > 0 && page > histTotalPages) return;
  291. histPage = page;
  292. const params = new URLSearchParams();
  293. const addr = document.getElementById('hist-device').value;
  294. const start = document.getElementById('hist-start').value;
  295. const end = document.getElementById('hist-end').value;
  296. const size = document.getElementById('hist-size').value;
  297. const sort = document.getElementById('hist-sort').value;
  298. if (addr !== '0') params.set('device_addr', addr);
  299. if (start) params.set('start_time', start.replace('T', ' '));
  300. if (end) params.set('end_time', end.replace('T', ' '));
  301. params.set('page', page);
  302. params.set('page_size', size);
  303. params.set('sort', sort);
  304. try {
  305. const res = await fetch('/api/history?' + params.toString());
  306. const data = await res.json();
  307. histTotalPages = data.total_pages || 0;
  308. // Update table
  309. const tbody = document.getElementById('history-tbody');
  310. tbody.innerHTML = '';
  311. (data.records || []).forEach(r => {
  312. const faultClass = r.fault_code === 20 ? 'fault-normal' : 'fault-warning';
  313. tbody.innerHTML += `<tr>
  314. <td>${r.timestamp}</td><td>#${r.device_addr}</td>
  315. <td>${r.battery_voltage?.toFixed(1)} V</td><td>${r.battery_soc} %</td>
  316. <td>${r.pv_voltage?.toFixed(1)} V</td><td>${r.pv_charge_current?.toFixed(1)} A</td>
  317. <td>${r.pv_input_power?.toFixed(1)} W</td><td>${r.machine_temp >= 0 ? r.machine_temp + '°C' : 'Fault'}</td>
  318. <td class="${faultClass}">${r.fault_desc}</td>
  319. <td>${r.charge_mode_desc || '-'}</td>
  320. <td>${r.battery_type_desc || '-'}</td>
  321. <td>${r.cumulative_energy?.toFixed(1)} kWh</td>
  322. </tr>`;
  323. });
  324. // Update pagination
  325. document.getElementById('hist-info').textContent =
  326. `Page ${data.page}/${data.total_pages} | Total ${data.total} records`;
  327. document.getElementById('btn-prev').disabled = page <= 1;
  328. document.getElementById('btn-next').disabled = page >= histTotalPages;
  329. // Update chart
  330. updateChart(data.records || []);
  331. } catch(e) {
  332. console.error('History fetch failed:', e);
  333. }
  334. }
  335. function updateChart(records) {
  336. const ctx = document.getElementById('history-chart').getContext('2d');
  337. const labels = records.map(r => r.timestamp?.slice(5) || '').reverse();
  338. const battV = records.map(r => r.battery_voltage).reverse();
  339. const soc = records.map(r => r.battery_soc).reverse();
  340. const power = records.map(r => r.pv_input_power).reverse();
  341. const temp = records.map(r => r.machine_temp).reverse();
  342. if (historyChart) historyChart.destroy();
  343. historyChart = new Chart(ctx, {
  344. type: 'line',
  345. data: {
  346. labels,
  347. datasets: [
  348. {label:'Battery (V)',data:battV,borderColor:'#3b82f6',backgroundColor:'rgba(59,130,246,.1)',yAxisID:'y',tension:.3,pointRadius:1},
  349. {label:'SOC (%)',data:soc,borderColor:'#22c55e',backgroundColor:'rgba(34,197,94,.1)',yAxisID:'y1',tension:.3,pointRadius:1},
  350. {label:'PV Power (W)',data:power,borderColor:'#f59e0b',backgroundColor:'rgba(245,158,11,.1)',yAxisID:'y',tension:.3,pointRadius:1},
  351. {label:'Temp (°C)',data:temp,borderColor:'#ef4444',backgroundColor:'rgba(239,68,68,.1)',yAxisID:'y',tension:.3,pointRadius:1}
  352. ]
  353. },
  354. options: {
  355. responsive:true,maintainAspectRatio:false,
  356. interaction:{mode:'index',intersect:false},
  357. plugins:{legend:{labels:{color:'#94a3b8',font:{size:11}}}},
  358. scales:{
  359. x:{ticks:{color:'#64748b',maxTicksLimit:12,font:{size:10}},grid:{color:'#1e293b'}},
  360. y:{type:'linear',position:'left',ticks:{color:'#64748b',font:{size:10}},grid:{color:'#1e293b'},title:{display:true,text:'V / W / °C',color:'#64748b'}},
  361. y1:{type:'linear',position:'right',min:0,max:100,ticks:{color:'#64748b',font:{size:10}},grid:{drawOnChartArea:false},title:{display:true,text:'SOC %',color:'#64748b'}}
  362. }
  363. }
  364. });
  365. }
  366. // Init
  367. fetchRealtime();
  368. fetchStatus();
  369. setInterval(fetchRealtime, 2000);
  370. setInterval(fetchStatus, 10000);
  371. // Set default time range
  372. const now = new Date();
  373. const start = new Date(now.getTime() - 24*3600000);
  374. document.getElementById('hist-end').value = now.toISOString().slice(0,16);
  375. document.getElementById('hist-start').value = start.toISOString().slice(0,16);
  376. // ===== Settings Page Functions =====
  377. const paramInfo = {
  378. battery_type: {desc:'电池类型', range:'0-5', options:'0=胶体 1=密封 2=磷酸铁锂 3=三元锂电 4=自定义 5=加水', note:'注意: 芯片擦写次数最大100000次,不要频繁设置'},
  379. battery_level: {desc:'电池电压等级', range:'0-6', options:'0=自动识别 1=12V 2=24V 3=36V 4=48V 5=60V 6=72V', note:'自定义电池类型时忽略此参数'},
  380. charge_current_limit: {desc:'光伏充电电流限制', range:'5-100', options:'单位: %', note:'限制光伏最大充电电流百分比'},
  381. full_charge_voltage: {desc:'电池充满电压', range:'80-600', options:'单位: 0.1V (如280=28.0V)', note:'仅自定义电池类型(type=4)允许修改'},
  382. device_address: {desc:'设备485地址', range:'0-247', options:'Modbus设备地址', note:'修改后需使用新地址重新连接设备'},
  383. charge_mode: {desc:'充电运行模式', range:'0-1', options:'0=MPPT 1=DCDC', note:'MPPT=最大功率点跟踪, DCDC=直流转换'}
  384. };
  385. function updateParamInfo() {
  386. const param = document.getElementById('set-param').value;
  387. const info = paramInfo[param];
  388. document.getElementById('param-info').innerHTML =
  389. `<strong>${info.desc}</strong> | 范围: ${info.range} | ${info.options}<br><span style="color:#eab308">⚠ ${info.note}</span>`;
  390. }
  391. function updateSettingsDeviceList() {
  392. fetch('/api/data').then(r=>r.json()).then(data => {
  393. const sel = document.getElementById('set-device');
  394. sel.innerHTML = '';
  395. data.devices.forEach(dev => {
  396. const opt = document.createElement('option');
  397. opt.value = dev.device_addr;
  398. opt.textContent = `Device #${dev.device_addr} ${dev.online ? '(ONLINE)' : '(OFFLINE)'}`;
  399. sel.appendChild(opt);
  400. });
  401. });
  402. updateParamInfo();
  403. }
  404. function writeSetting() {
  405. const addr = parseInt(document.getElementById('set-device').value);
  406. const param = document.getElementById('set-param').value;
  407. const value = parseInt(document.getElementById('set-value').value);
  408. const resultDiv = document.getElementById('write-result');
  409. if (isNaN(addr) || isNaN(value)) {
  410. resultDiv.style.display = 'block';
  411. resultDiv.style.background = '#7f1d1d';
  412. resultDiv.style.color = '#fca5a5';
  413. resultDiv.textContent = '请输入有效的设备地址和数值';
  414. return;
  415. }
  416. if (!confirm(`确认写入?\n设备: #${addr}\n参数: ${param}\n值: ${value}\n\n注意: 频繁写入可能损坏芯片(最大100000次)`)) {
  417. return;
  418. }
  419. const btn = document.getElementById('btn-write');
  420. btn.disabled = true;
  421. btn.textContent = 'Writing...';
  422. fetch('/api/settings', {
  423. method: 'POST',
  424. headers: {'Content-Type': 'application/json'},
  425. body: JSON.stringify({device_addr: addr, param: param, value: value})
  426. })
  427. .then(r => r.json())
  428. .then(data => {
  429. resultDiv.style.display = 'block';
  430. if (data.success) {
  431. resultDiv.style.background = '#065f46';
  432. resultDiv.style.color = '#6ee7b7';
  433. resultDiv.innerHTML = `✓ 写入成功! ${data.param_desc} = ${data.value}`;
  434. } else {
  435. resultDiv.style.background = '#7f1d1d';
  436. resultDiv.style.color = '#fca5a5';
  437. resultDiv.textContent = `✗ 写入失败: ${data.error}`;
  438. }
  439. })
  440. .catch(err => {
  441. resultDiv.style.display = 'block';
  442. resultDiv.style.background = '#7f1d1d';
  443. resultDiv.style.color = '#fca5a5';
  444. resultDiv.textContent = `✗ 请求失败: ${err.message}`;
  445. })
  446. .finally(() => {
  447. btn.disabled = false;
  448. btn.textContent = 'Write';
  449. });
  450. }
  451. </script>
  452. </body>
  453. </html>