mirror of
https://github.com/Laxilef/OTGateway.git
synced 2025-12-26 01:53:35 +05:00
2 points Calibration
This commit is contained in:
@@ -347,6 +347,13 @@
|
|||||||
"title": "T factor",
|
"title": "T factor",
|
||||||
"note": "Not used if PID is enabled"
|
"note": "Not used if PID is enabled"
|
||||||
},
|
},
|
||||||
|
"calibration": "2 points Calibration",
|
||||||
|
"obs1_outdoor": "Warm Point Outdoor",
|
||||||
|
"obs1_radiator": "Warm Point Radiator",
|
||||||
|
"obs2_outdoor": "Cold Point Outdoor",
|
||||||
|
"obs2_radiator": "Cold Point Radiator",
|
||||||
|
"calibrate": "Calibrate",
|
||||||
|
|
||||||
"chart": {
|
"chart": {
|
||||||
"radiatorTemp": "Radiator Temperature (°C)",
|
"radiatorTemp": "Radiator Temperature (°C)",
|
||||||
"outdoorTemp": "Outdoor Temperature (°C)"
|
"outdoorTemp": "Outdoor Temperature (°C)"
|
||||||
|
|||||||
@@ -347,6 +347,12 @@
|
|||||||
"title": "Fattore T",
|
"title": "Fattore T",
|
||||||
"note": "Non usato se PID è attivato"
|
"note": "Non usato se PID è attivato"
|
||||||
},
|
},
|
||||||
|
"calibration": "Calibrazione a 2 punti",
|
||||||
|
"obs1_outdoor": "Punto caldo esterno",
|
||||||
|
"obs1_radiator": "Punto caldo (radiatore)",
|
||||||
|
"obs2_outdoor": "Punto freddo esterno",
|
||||||
|
"obs2_radiator": "Punto freddo (radiatore)",
|
||||||
|
"calibrate": "Calibra",
|
||||||
"chart": {
|
"chart": {
|
||||||
"radiatorTemp": "Temperatura Del Radiatore (°C)",
|
"radiatorTemp": "Temperatura Del Radiatore (°C)",
|
||||||
"outdoorTemp": "Outdoor Temperature (°C)"
|
"outdoorTemp": "Outdoor Temperature (°C)"
|
||||||
|
|||||||
@@ -347,6 +347,12 @@
|
|||||||
"title": "Коэффициент T",
|
"title": "Коэффициент T",
|
||||||
"note": "Не используется, если ПИД включен"
|
"note": "Не используется, если ПИД включен"
|
||||||
},
|
},
|
||||||
|
"calibration": "Калибровка по двум точкам",
|
||||||
|
"obs1_outdoor": "Теплая точка (снаружи)",
|
||||||
|
"obs1_radiator": "Теплая точка (радиатор)",
|
||||||
|
"obs2_outdoor": "Холодная точка (снаружи)",
|
||||||
|
"obs2_radiator": "Холодная точка (радиатор)",
|
||||||
|
"calibrate": "Калибровать",
|
||||||
"chart": {
|
"chart": {
|
||||||
"radiatorTemp": "Температура радиатора (°C)",
|
"radiatorTemp": "Температура радиатора (°C)",
|
||||||
"outdoorTemp": "Наружная температура (°C)"
|
"outdoorTemp": "Наружная температура (°C)"
|
||||||
|
|||||||
@@ -274,30 +274,59 @@
|
|||||||
<span data-i18n>settings.enable</span>
|
<span data-i18n>settings.enable</span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n>settings.equitherm.n</span>
|
<span data-i18n>settings.equitherm.n</span>
|
||||||
<input type="number" inputmode="decimal" name="equitherm[n_factor]" min="0.001" max="10" step="0.001" required>
|
<input type="number" inputmode="decimal" name="equitherm[n_factor]" min="0.001" max="10" step="0.001"
|
||||||
|
required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n>settings.equitherm.k</span>
|
<span data-i18n>settings.equitherm.k</span>
|
||||||
<input type="number" inputmode="decimal" name="equitherm[k_factor]" min="-15" max="5" step="0.01" required>
|
<input type="number" inputmode="decimal" name="equitherm[k_factor]" min="-15" max="5" step="0.01" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n>settings.equitherm.e</span>
|
<span data-i18n>settings.equitherm.e</span>
|
||||||
<input type="number" inputmode="decimal" name="equitherm[e_factor]" min="1" max="2" step="0.01" required>
|
<input type="number" inputmode="decimal" name="equitherm[e_factor]" min="1" max="2" step="0.01" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n>settings.equitherm.t.title</span>
|
<span data-i18n>settings.equitherm.t.title</span>
|
||||||
<input type="number" inputmode="decimal" name="equitherm[t_factor]" min="0" max="10" step="0.01" required>
|
<input type="number" inputmode="decimal" name="equitherm[t_factor]" min="0" max="10" step="0.01" required>
|
||||||
<small data-i18n>settings.equitherm.t.note</small>
|
<small data-i18n>settings.equitherm.t.note</small>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<legend><b data-i18n>settings.equitherm.calibration</b></legend>
|
||||||
|
<div class="grid">
|
||||||
|
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span data-i18n>settings.equitherm.obs1_outdoor</span>
|
||||||
|
<input type="number" inputmode="decimal" name="equitherm[obs1_outdoor]" min="-50" max="50" step="0.1"
|
||||||
|
required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span data-i18n>settings.equitherm.obs1_radiator</span>
|
||||||
|
<input type="number" inputmode="decimal" name="equitherm[obs1_radiator]" min="10" max="90" step="0.1"
|
||||||
|
required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span data-i18n>settings.equitherm.obs2_outdoor</span>
|
||||||
|
<input type="number" inputmode="decimal" name="equitherm[obs2_outdoor]" min="-50" max="50" step="0.1"
|
||||||
|
required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span data-i18n>settings.equitherm.obs2_radiator</span>
|
||||||
|
<input type="number" inputmode="decimal" name="equitherm[obs2_radiator]" min="10" max="90" step="0.1"
|
||||||
|
required>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="calibrateEquitherm" data-i18n>settings.equitherm.calibrate</button>
|
||||||
<button type="submit" data-i18n>button.save</button>
|
<button type="submit" data-i18n>button.save</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -978,87 +1007,89 @@
|
|||||||
//График
|
//График
|
||||||
let equithermChart;
|
let equithermChart;
|
||||||
|
|
||||||
|
|
||||||
async function fetchSettings() {
|
async function fetchSettings() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/settings", {
|
const response = await fetch("/api/settings", {
|
||||||
cache: "no-cache",
|
cache: "no-cache",
|
||||||
credentials: "include"
|
credentials: "include"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Response not valid');
|
throw new Error("Response not valid");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Считаем температуру
|
|
||||||
function calculateTRad(targetTemp, outdoorTemp, maxOut, Kn, Ke, Kk) {
|
function calculateTRad(targetTemp, outdoorTemp, maxOut, Kn, Ke, Kk) {
|
||||||
let tempDelta = targetTemp - outdoorTemp;
|
let tempDelta = targetTemp - outdoorTemp;
|
||||||
const maxPoint = targetTemp - (maxOut - targetTemp) / Kn;
|
const maxPoint = targetTemp - (maxOut - targetTemp) / Kn;
|
||||||
let base = targetTemp - maxPoint;
|
let base = targetTemp - maxPoint;
|
||||||
|
|
||||||
if (base <= 0) {
|
if (base <= 0) {
|
||||||
base = 0.0001;
|
base = 0.0001;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sf = (maxOut - targetTemp) / Math.pow(base, 1.0 / Ke);
|
const sf = (maxOut - targetTemp) / Math.pow(base, 1.0 / Ke);
|
||||||
|
let T_rad =
|
||||||
let T_rad = targetTemp + sf * (tempDelta >= 0 ? Math.pow(tempDelta, 1.0 / Ke) : -Math.pow(-tempDelta, 1.0 / Ke)) + Kk;
|
targetTemp +
|
||||||
|
sf * (tempDelta >= 0 ? Math.pow(tempDelta, 1.0 / Ke) : -Math.pow(-tempDelta, 1.0 / Ke)) +
|
||||||
|
Kk;
|
||||||
return Math.min(T_rad, maxOut);
|
return Math.min(T_rad, maxOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем данные для графика
|
|
||||||
function generateChartData(targetTemp, maxOut, Kn, Ke, Kk) {
|
function generateChartData(targetTemp, maxOut, Kn, Ke, Kk) {
|
||||||
const outdoorTemps = [];
|
const outdoorTemps = [];
|
||||||
const predictedTRad = [];
|
const predictedTRad = [];
|
||||||
|
|
||||||
for (let temp = 25; temp >= -30; temp -= 1) {
|
for (let temp = 25; temp >= -30; temp -= 1) {
|
||||||
outdoorTemps.push(temp);
|
outdoorTemps.push(temp);
|
||||||
predictedTRad.push(calculateTRad(targetTemp, temp, maxOut, Kn, Ke, Kk).toFixed(1));
|
predictedTRad.push(calculateTRad(targetTemp, temp, maxOut, Kn, Ke, Kk).toFixed(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { outdoorTemps, predictedTRad };
|
return { outdoorTemps, predictedTRad };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем график
|
function createChart(outdoorTemps, predictedTRad, obsPoints = []) {
|
||||||
function createChart(outdoorTemps, predictedTRad) {
|
const ctx = document.getElementById("equithermChart").getContext("2d");
|
||||||
const ctx = document.getElementById('equithermChart').getContext('2d');
|
|
||||||
|
|
||||||
const canvasHeight = ctx.canvas.height;
|
const canvasHeight = ctx.canvas.height;
|
||||||
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, 0);
|
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, 0);
|
||||||
gradient.addColorStop(0, 'rgba(75, 192, 192, 1)');
|
gradient.addColorStop(0, "rgba(75, 192, 192, 1)");
|
||||||
gradient.addColorStop(0.5, 'rgba(255, 99, 132, 1)');
|
gradient.addColorStop(0.5, "rgba(255, 99, 132, 1)");
|
||||||
|
|
||||||
equithermChart = new Chart(ctx, {
|
equithermChart = new Chart(ctx, {
|
||||||
type: 'line',
|
type: "line",
|
||||||
data: {
|
data: {
|
||||||
labels: outdoorTemps,
|
labels: outdoorTemps,
|
||||||
datasets: [{
|
datasets: [
|
||||||
label: 'Температура Радиатора (°C)',
|
{
|
||||||
borderColor: gradient,
|
label: "Температура Радиатора (°C)",
|
||||||
borderWidth: 1,
|
borderColor: gradient,
|
||||||
fill: false,
|
borderWidth: 1,
|
||||||
tension: 0.1,
|
fill: false,
|
||||||
pointRadius: 2,
|
tension: 0.1,
|
||||||
pointHoverRadius: 4,
|
pointRadius: 2,
|
||||||
data: predictedTRad
|
pointHoverRadius: 4,
|
||||||
}]
|
data: predictedTRad
|
||||||
|
},
|
||||||
|
// Точки калибровки
|
||||||
|
{
|
||||||
|
label: "Наблюдаемые точки",
|
||||||
|
type: "scatter",
|
||||||
|
data: obsPoints,
|
||||||
|
backgroundColor: "gold",
|
||||||
|
borderColor: "gold",
|
||||||
|
pointRadius: 6,
|
||||||
|
pointHoverRadius: 8
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
interaction: {
|
interaction: {
|
||||||
mode: 'nearest',
|
mode: "nearest",
|
||||||
intersect: false
|
intersect: false
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
position: 'nearest',
|
position: "nearest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
@@ -1066,14 +1097,14 @@
|
|||||||
display: true,
|
display: true,
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Наружная температура (°C)'
|
text: "Наружная температура (°C)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
display: true,
|
display: true,
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Температура Радиатора (°C)'
|
text: "Температура Радиатора (°C)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1081,54 +1112,111 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализируем график
|
|
||||||
async function initChart() {
|
async function initChart() {
|
||||||
try {
|
try {
|
||||||
const result = await fetchSettings();
|
const result = await fetchSettings();
|
||||||
|
|
||||||
const { heating, equitherm } = result;
|
const { heating, equitherm } = result;
|
||||||
const targetTemp = heating?.target ?? 24;
|
const targetTemp = heating?.target ?? 24;
|
||||||
const maxOut = heating?.maxTemp ?? 90;
|
const maxOut = heating?.maxTemp ?? 90;
|
||||||
const Kn = equitherm?.n_factor ?? 1;
|
const Kn = equitherm?.n_factor ?? 1;
|
||||||
const Ke = equitherm?.e_factor ?? 1.3;
|
const Ke = equitherm?.e_factor ?? 1.3;
|
||||||
const Kk = equitherm?.k_factor ?? 0;
|
const Kk = equitherm?.k_factor ?? 0;
|
||||||
|
|
||||||
|
|
||||||
const { outdoorTemps, predictedTRad } = generateChartData(targetTemp, maxOut, Kn, Ke, Kk);
|
const { outdoorTemps, predictedTRad } = generateChartData(targetTemp, maxOut, Kn, Ke, Kk);
|
||||||
|
|
||||||
|
|
||||||
createChart(outdoorTemps, predictedTRad);
|
createChart(outdoorTemps, predictedTRad);
|
||||||
|
document.getElementById("equitherm-settings-busy").classList.add("hidden");
|
||||||
|
document.getElementById("equitherm-settings").classList.remove("hidden");
|
||||||
document.getElementById('equitherm-settings-busy').classList.add('hidden');
|
|
||||||
document.getElementById('equitherm-settings').classList.remove('hidden');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateChart(formData) {
|
function updateChart(formData) {
|
||||||
if (!equithermChart) return;
|
if (!equithermChart) return;
|
||||||
|
|
||||||
fetchSettings()
|
fetchSettings()
|
||||||
.then(result => {
|
.then(result => {
|
||||||
const targetTemp = result?.heating?.target ?? 24;
|
const targetTemp = result?.heating?.target ?? 24;
|
||||||
const maxOut = result?.heating?.maxTemp ?? 90;
|
const maxOut = result?.heating?.maxTemp ?? 90;
|
||||||
|
const Kn = parseFloat(formData.get("equitherm[n_factor]")) || 1;
|
||||||
const Kn = parseFloat(formData.get('equitherm[n_factor]')) || 1;
|
const Ke = parseFloat(formData.get("equitherm[e_factor]")) || 1.3;
|
||||||
const Ke = parseFloat(formData.get('equitherm[e_factor]')) || 1.3;
|
const Kk = parseFloat(formData.get("equitherm[k_factor]")) || 0;
|
||||||
const Kk = parseFloat(formData.get('equitherm[k_factor]')) || 0;
|
|
||||||
|
|
||||||
const { outdoorTemps, predictedTRad } = generateChartData(targetTemp, maxOut, Kn, Ke, Kk);
|
const { outdoorTemps, predictedTRad } = generateChartData(targetTemp, maxOut, Kn, Ke, Kk);
|
||||||
|
// Точки калибровки
|
||||||
|
let obsPoints = [];
|
||||||
|
const obs1Out = parseFloat(formData.get("equitherm[obs1_outdoor]"));
|
||||||
|
const obs1Rad = parseFloat(formData.get("equitherm[obs1_radiator]"));
|
||||||
|
const obs2Out = parseFloat(formData.get("equitherm[obs2_outdoor]"));
|
||||||
|
const obs2Rad = parseFloat(formData.get("equitherm[obs2_radiator]"));
|
||||||
|
if (!isNaN(obs1Out) && !isNaN(obs1Rad) && !isNaN(obs2Out) && !isNaN(obs2Rad)) {
|
||||||
|
obsPoints.push({ x: obs1Out, y: obs1Rad });
|
||||||
|
obsPoints.push({ x: obs2Out, y: obs2Rad });
|
||||||
|
}
|
||||||
equithermChart.data.labels = outdoorTemps;
|
equithermChart.data.labels = outdoorTemps;
|
||||||
equithermChart.data.datasets[0].data = predictedTRad;
|
equithermChart.data.datasets[0].data = predictedTRad;
|
||||||
|
if (equithermChart.data.datasets.length > 1) {
|
||||||
|
equithermChart.data.datasets[1].data = obsPoints;
|
||||||
|
}
|
||||||
equithermChart.update();
|
equithermChart.update();
|
||||||
})
|
})
|
||||||
.catch(error => console.log(error));
|
.catch(error => console.log(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Калибровка
|
||||||
|
async function fetchAndCalibrate() {
|
||||||
|
try {
|
||||||
|
const result = await fetchSettings();
|
||||||
|
const { heating, equitherm } = result;
|
||||||
|
const targetTemp = heating?.target ?? 24;
|
||||||
|
const maxOut = heating?.maxTemp ?? 90;
|
||||||
|
const Ke = equitherm?.e_factor ?? 1.3;
|
||||||
|
|
||||||
|
const form = document.getElementById("equitherm-settings");
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
const obs1Out = parseFloat(formData.get("equitherm[obs1_outdoor]"));
|
||||||
|
const obs1Rad = parseFloat(formData.get("equitherm[obs1_radiator]"));
|
||||||
|
const obs2Out = parseFloat(formData.get("equitherm[obs2_outdoor]"));
|
||||||
|
const obs2Rad = parseFloat(formData.get("equitherm[obs2_radiator]"));
|
||||||
|
|
||||||
|
if (isNaN(obs1Out) || isNaN(obs1Rad) || isNaN(obs2Out) || isNaN(obs2Rad)) {
|
||||||
|
alert("Please enter valid observation points.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем чтобы наружн. меньше целевой
|
||||||
|
const diff1 = targetTemp - obs1Out;
|
||||||
|
const diff2 = targetTemp - obs2Out;
|
||||||
|
if (diff1 <= 0 || diff2 <= 0) {
|
||||||
|
alert("Outdoor temperature must be below the target temperature.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A
|
||||||
|
const denominator = Math.pow(diff2, 1 / Ke) - Math.pow(diff1, 1 / Ke);
|
||||||
|
if (denominator === 0) {
|
||||||
|
alert("Calibration error: Denominator is zero.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const A = (obs2Rad - obs1Rad) / denominator;
|
||||||
|
|
||||||
|
// Считаем N из A
|
||||||
|
const N = Math.pow(A / Math.pow(maxOut - targetTemp, 1 - 1 / Ke), Ke);
|
||||||
|
|
||||||
|
// Считаем K по первой точке
|
||||||
|
const K = obs1Rad - targetTemp - A * Math.pow(diff1, 1 / Ke);
|
||||||
|
|
||||||
|
// Обновляем поля
|
||||||
|
document.querySelector("input[name='equitherm[n_factor]']").value = N.toFixed(3);
|
||||||
|
document.querySelector("input[name='equitherm[k_factor]']").value = K.toFixed(2);
|
||||||
|
|
||||||
|
// Обновляем график
|
||||||
|
updateChart(new FormData(form));
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error during calibration:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Кнопка
|
||||||
|
const calibrateButton = document.getElementById("calibrateEquitherm");
|
||||||
|
calibrateButton.addEventListener("click", fetchAndCalibrate);
|
||||||
|
|
||||||
// Слушаем отправку
|
// Слушаем отправку
|
||||||
const form = document.getElementById('equitherm-settings');
|
const form = document.getElementById('equitherm-settings');
|
||||||
|
|||||||
Reference in New Issue
Block a user