Add a page for sending custom OT requests

This commit is contained in:
Roman Andriadi
2025-02-03 14:43:36 +00:00
parent 80b91d9a01
commit 0824066897
7 changed files with 370 additions and 1 deletions

View File

@@ -29,7 +29,8 @@
"wait": "Please wait...",
"uploading": "Uploading...",
"success": "Success",
"error": "Error"
"error": "Error",
"send": "Send"
},
"index": {
@@ -467,6 +468,31 @@
"settingsFile": "Settings file",
"fw": "Firmware",
"fs": "Filesystem"
},
"opentherm_request": {
"title": "Custom requests - OpenTherm Gateway",
"name": "Custom requests",
"section": {
"read": "Read",
"read.desc": "Send a read request with a custom message ID"
},
"messageId": "Message ID",
"result": {
"valid": "Valid",
"parityValid": "Parity valid",
"responseMessageIdValid": "Response message ID valid",
"responseType": "Response type",
"data": "Data value",
"flags": "As flags",
"hex": "As hex",
"fixedPoint": "As f8.8",
"u8": "As two u8",
"s8": "As two s8",
"u16": "As u16",
"s16": "As s16"
}
}
}
}

View File

@@ -146,6 +146,7 @@
<a href="/settings.html" role="button" data-i18n>settings.name</a>
<a href="/sensors.html" role="button" data-i18n>sensors.name</a>
<a href="/upgrade.html" role="button" data-i18n>upgrade.name</a>
<a href="/opentherm_request.html" role="button" data-i18n>opentherm_request.name</a>
<a href="/restart.html" role="button" class="secondary restart" data-i18n>button.restart</a>
</div>
</div>

View File

@@ -0,0 +1,132 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title data-i18n>opentherm_request.title</title>
<link rel="stylesheet" href="/static/app.css?{BUILD_TIME}">
</head>
<body>
<header class="container">
<nav>
<ul>
<li>
<a href="/">
<div class="logo" data-i18n>logo</div>
</a>
</li>
</ul>
<ul>
<li>
<select id="lang" aria-label="Lang">
<option value="en" selected>EN</option>
<option value="it">IT</option>
<option value="ru">RU</option>
</select>
</li>
</ul>
</nav>
</header>
<main class="container">
<article>
<div>
<hgroup>
<h2 data-i18n>opentherm_request.section.read</h2>
<p data-i18n>opentherm_request.section.read.desc</p>
</hgroup>
<form action="/api/opentherm_request/read" id="read">
<label for="message_id">
<span data-i18n>opentherm_request.messageId</span>
<input type="number" inputmode="numeric" name="messageId" id="message-id" min="0" max="255" step="1" required>
</label>
<div role="group">
<button type="submit" data-i18n>button.send</button>
</div>
</form>
<div role="group" id="read-result">
<table>
<tbody>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.valid</th>
<td><i class="mValid"></i></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.parityValid</th>
<td><i class="mParityValid"></i></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.responseMessageIdValid</th>
<td><i class="mResponseMessageIdValid"></i></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.responseType</th>
<td><b class="mResponseType"></b></td>
</tr>
<tr>
<th colspan="3" scope="row"><span data-i18n>opentherm_request.result.data</span>:</th>
</tr>
<tr>
<th scope="row" data-i18n>opentherm_request.result.flags</th>
<td><b class="mDataFlagsHigh"></b></td>
<td><b class="mDataFlagsLow"></b></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.hex</th>
<td><b class="mDataHex"></b></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.fixedPoint</th>
<td><b class="mDataFixedPoint"></b></td>
</tr>
<tr>
<th scope="row" data-i18n>opentherm_request.result.u8</th>
<td><b class="mDataU8High"></b></td>
<td><b class="mDataU8Low"></b></td>
</tr>
<tr>
<th scope="row" data-i18n>opentherm_request.result.s8</th>
<td><b class="mDataS8High"></b></td>
<td><b class="mDataS8Low"></b></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.u16</th>
<td><b class="mDataU16"></b></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.s16</th>
<td><b class="mDataS16"></b></td>
</tr>
</tbody>
</table>
</div>
</div>
</article>
</main>
<footer class="container">
<small>
<b>Made by Laxilef</b>
<a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary" data-i18n>nav.license</a>
<a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary" data-i18n>nav.source</a>
<a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary" data-i18n>nav.help</a>
<a href="https://github.com/Laxilef/OTGateway/issues" target="_blank" class="secondary" data-i18n>nav.issues</a>
<a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary" data-i18n>nav.releases</a>
</small>
</footer>
<script src="/static/app.js?{BUILD_TIME}"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
const lang = new Lang(document.getElementById('lang'));
lang.build();
setupOTReadForm('#read');
});
</script>
</body>
</html>

View File

@@ -522,6 +522,107 @@ const setupUpgradeForm = (formSelector) => {
});
}
const setupOTReadForm = (formSelector) => {
const form = document.querySelector(formSelector);
if (!form) {
return;
}
const url = form.action;
let button = form.querySelector('button[type="submit"]');
let defaultText;
hide("#read-result");
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (button) {
defaultText = button.textContent;
button.textContent = i18n('button.wait');
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
hide("#read-result");
const onSuccess = (result) => {
if (button) {
button.textContent = i18n('button.success');
button.classList.add('success');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 2000);
}
setState(".mValid", result.valid);
setState(".mParityValid", result.parityValid);
setState(".m", );
setState(".mResponseMessageIdValid", result.responseMessageIdValid);
setValue(".mResponseType", result.responseType);
const u16 = result.value;
const [flagsHigh, flagsLow] = u16ToFlags(u16);
const hex = u16ToHex(u16);
const fixedPoint = u16ToFixedPoint(u16);
const [u8High, u8Low] = u16ToU8s(u16);
const [s8High, s8Low] = u16ToS8s(u16);
const s16 = u16ToS16(u16);
setValue(".mDataFlagsHigh", flagsHigh);
setValue(".mDataFlagsLow", flagsLow);
setValue(".mDataHex", hex);
setValue(".mDataFixedPoint", fixedPoint);
setValue(".mDataU8High", u8High);
setValue(".mDataU8Low", u8Low);
setValue(".mDataS8High", s8High);
setValue(".mDataS8Low", s8Low);
setValue(".mDataU16", u16);
setValue(".mDataS16", s16);
show("#read-result");
};
const onFailed = () => {
if (button) {
button.textContent = i18n('button.error');
button.classList.add('failed');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
const messageId = form.querySelector('#message-id').value;
try {
let fd = new FormData(form);
let response = await fetch(url, {
method: "POST",
cache: "no-cache",
credentials: "include",
body: form2json(fd)
});
if (!response.ok) {
throw new Error('Response not valid');
}
const result = await response.json();
onSuccess(result);
} catch (err) {
onFailed(false);
}
});
}
const setBusy = (busySelector, contentSelector, value, parent = undefined) => {
if (!value) {
@@ -848,4 +949,36 @@ function dec2hex(i) {
}
return hex.toUpperCase();
}
function u16ToHex(i) {
return (i >>> 0).toString(16).padStart(4, "0").toUpperCase();
}
function u16ToU8s(i) {
let low = (i >>> 0) & 0xFF;
let high = ((i >>> 0) & 0xFF00) >> 8;
return [high, low];
}
function u16ToS8s(i) {
let [high, low] = u16ToU8s(i);
return [high << 24 >> 24, low << 24 >> 24];
}
function u16ToS16(i) {
return (i >>> 0) << 16 >> 16;
}
function u16ToFlags(i) {
let [high, low] = u16ToU8s(i);
return [
high.toString(2).padStart(8, "0"),
low.toString(2).padStart(8, "0")
];
}
function u16ToFixedPoint(i) {
let [high, low] = u16ToU8s(i);
return (high + low / 256).toFixed(3)
}