Update relay-handling logic and test assertions for device binding and local IP resolution
- Correct test cases to ensure proper relay IDs are switched. - Add robust local IP resolution for relay-device bindings, including caching and inventory backfill. - Introduce fast-path options for relay status and switch dispatch. - Validate PHP extensions (`curl`, `sqlite3`) in edge agent images.
This commit is contained in:
Vendored
+67
-9
@@ -17,6 +17,7 @@ const DEFAULT_UPDATE_VERIFICATION_TIMEOUT_SECONDS = 45;
|
||||
const DEFAULT_UPDATE_VERIFY_INTERVAL_MS = 500;
|
||||
const DEFAULT_UPDATE_RESTART_GRACE_MS = 150;
|
||||
const DEFAULT_BROKER_RECONNECT_DELAY_MS = 1500;
|
||||
const DEFAULT_SHELLY_LOCAL_HTTP_TIMEOUT_MS = 1200;
|
||||
const UPDATE_VERIFY_COMMAND = "post-update-verify";
|
||||
const execFile = promisify(execFileCallback);
|
||||
|
||||
@@ -426,8 +427,55 @@ export async function claimIfNeeded(config, configPath, fetchImpl = fetch) {
|
||||
return nextConfig;
|
||||
}
|
||||
|
||||
async function fetchJson(url, fetchImpl = fetch) {
|
||||
const response = await fetchImpl(url);
|
||||
function makeHttpTimeoutError(timeoutMs) {
|
||||
const error = new Error(`HTTP request timed out after ${timeoutMs}ms`);
|
||||
error.code = "EDGE_AGENT_HTTP_TIMEOUT";
|
||||
return error;
|
||||
}
|
||||
|
||||
function isHttpTimeoutError(error) {
|
||||
return error?.code === "EDGE_AGENT_HTTP_TIMEOUT";
|
||||
}
|
||||
|
||||
function resolveShellyLocalHttpTimeoutMs(options = {}) {
|
||||
const configured = Number(options.timeoutMs ?? process.env.EDGE_SHELLY_LOCAL_HTTP_TIMEOUT_MS);
|
||||
if (Number.isFinite(configured) && configured > 0) {
|
||||
return Math.max(50, Math.floor(configured));
|
||||
}
|
||||
|
||||
return DEFAULT_SHELLY_LOCAL_HTTP_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
async function fetchJson(url, fetchImpl = fetch, options = {}) {
|
||||
const timeoutMs = Number(options.timeoutMs || 0);
|
||||
let timeout = null;
|
||||
let controller = null;
|
||||
const requestOptions = {};
|
||||
if (timeoutMs > 0 && typeof AbortController !== "undefined") {
|
||||
controller = new AbortController();
|
||||
requestOptions.signal = controller.signal;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
const fetchPromise = Promise.resolve().then(() => fetchImpl(url, requestOptions));
|
||||
response = timeoutMs > 0
|
||||
? await Promise.race([
|
||||
fetchPromise,
|
||||
new Promise((_, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
controller?.abort();
|
||||
reject(makeHttpTimeoutError(timeoutMs));
|
||||
}, timeoutMs);
|
||||
}),
|
||||
])
|
||||
: await fetchPromise;
|
||||
} finally {
|
||||
if (timeout !== null) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
@@ -458,7 +506,9 @@ export async function discoverShellyDevices(options = {}, fetchImpl = fetch) {
|
||||
|
||||
await Promise.all(candidateIps.map(async (ip) => {
|
||||
try {
|
||||
const identity = await fetchJson(`http://${ip}/shelly`, fetchImpl);
|
||||
const identity = await fetchJson(`http://${ip}/shelly`, fetchImpl, {
|
||||
timeoutMs: resolveShellyLocalHttpTimeoutMs(options),
|
||||
});
|
||||
discovered.push({
|
||||
id: identity.mac || identity.id || ip,
|
||||
device_id: identity.mac || identity.id || ip,
|
||||
@@ -482,19 +532,23 @@ export async function discoverShellyDevices(options = {}, fetchImpl = fetch) {
|
||||
export async function getRelayStatus(payload, fetchImpl = fetch) {
|
||||
const ip = payload.localIp || payload.local_ip || payload.ip;
|
||||
const channel = Number.isInteger(payload.channel) ? payload.channel : 0;
|
||||
const timeoutMs = resolveShellyLocalHttpTimeoutMs(payload);
|
||||
if (!ip) {
|
||||
throw new Error("Missing relay local IP");
|
||||
}
|
||||
|
||||
try {
|
||||
const rpc = await fetchJson(`http://${ip}/rpc/Switch.GetStatus?id=${channel}`, fetchImpl);
|
||||
const rpc = await fetchJson(`http://${ip}/rpc/Switch.GetStatus?id=${channel}`, fetchImpl, { timeoutMs });
|
||||
return {
|
||||
online: true,
|
||||
on: Boolean(rpc.output),
|
||||
raw: rpc,
|
||||
};
|
||||
} catch {
|
||||
const legacy = await fetchJson(`http://${ip}/relay/${channel}`, fetchImpl);
|
||||
} catch (error) {
|
||||
if (isHttpTimeoutError(error)) {
|
||||
throw error;
|
||||
}
|
||||
const legacy = await fetchJson(`http://${ip}/relay/${channel}`, fetchImpl, { timeoutMs });
|
||||
return {
|
||||
online: true,
|
||||
on: Boolean(legacy.ison ?? legacy.output),
|
||||
@@ -507,19 +561,23 @@ export async function setRelayState(payload, fetchImpl = fetch) {
|
||||
const ip = payload.localIp || payload.local_ip || payload.ip;
|
||||
const channel = Number.isInteger(payload.channel) ? payload.channel : 0;
|
||||
const on = Boolean(payload.on);
|
||||
const timeoutMs = resolveShellyLocalHttpTimeoutMs(payload);
|
||||
if (!ip) {
|
||||
throw new Error("Missing relay local IP");
|
||||
}
|
||||
|
||||
try {
|
||||
const rpc = await fetchJson(`http://${ip}/rpc/Switch.Set?id=${channel}&on=${on ? "true" : "false"}`, fetchImpl);
|
||||
const rpc = await fetchJson(`http://${ip}/rpc/Switch.Set?id=${channel}&on=${on ? "true" : "false"}`, fetchImpl, { timeoutMs });
|
||||
return {
|
||||
online: true,
|
||||
on: Boolean(rpc.output ?? on),
|
||||
raw: rpc,
|
||||
};
|
||||
} catch {
|
||||
const legacy = await fetchJson(`http://${ip}/relay/${channel}?turn=${on ? "on" : "off"}`, fetchImpl);
|
||||
} catch (error) {
|
||||
if (isHttpTimeoutError(error)) {
|
||||
throw error;
|
||||
}
|
||||
const legacy = await fetchJson(`http://${ip}/relay/${channel}?turn=${on ? "on" : "off"}`, fetchImpl, { timeoutMs });
|
||||
return {
|
||||
online: true,
|
||||
on: Boolean(legacy.ison ?? legacy.output ?? on),
|
||||
|
||||
Reference in New Issue
Block a user