diff --git a/client/components.js b/client/components.js index bfcbc13..458ed66 100644 --- a/client/components.js +++ b/client/components.js @@ -10,6 +10,7 @@ import deviceFaults from "./components/device-faults"; import allParameters from "./components/all-parameters"; import deviceActions from "./components/device-actions"; import tags from "./components/tags"; +import ping from "./components/ping"; function get(name) { if (name === "parameter") return parameter; @@ -22,6 +23,7 @@ function get(name) { else if (name === "all-parameters") return allParameters; else if (name === "device-actions") return deviceActions; else if (name === "tags") return tags; + else if (name === "ping") return ping; else throw new Error(`No such component '${name}'`); } diff --git a/client/components/ping.js b/client/components/ping.js new file mode 100644 index 0000000..89facae --- /dev/null +++ b/client/components/ping.js @@ -0,0 +1,54 @@ +"use strict"; + +import m from "mithril"; +import * as store from "../store"; + +const REFRESH_INTERVAL = 3000; + +const component = { + oninit: vnode => { + vnode.state.timestamp = 0; + vnode.state.ping = null; + vnode.state.host = null; + vnode.state.timeout = null; + }, + view: vnode => { + let refresh = () => { + const device = vnode.attrs.device; + let param = + device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]; + if (!param) + param = device["Device.ManagementServer.ConnectionRequestURL"]; + if (!param || !param.value) return; + + const url = new URL(param.value[0]); + vnode.state.host = url.hostname; + vnode.state.timestamp = Date.now(); + store + .ping(vnode.state.host) + .then(res => { + vnode.state.ping = res; + }) + .catch(() => { + // Do nothing + }); + }; + + let t = vnode.state.timestamp + REFRESH_INTERVAL - Date.now(); + clearTimeout(vnode.state.timeout); + if (t <= 0) refresh(); + else vnode.state.timeout = setTimeout(refresh, t); + + if (vnode.state.host) { + let status = ""; + if (vnode.state.ping) + if (vnode.state.ping.avg != null) + status = `${Math.trunc(vnode.state.ping.avg)} ms`; + else status = "Unreachable"; + + return m("div", `Pinging ${vnode.state.host}: ${status}`); + } + } +}; + +export default component; diff --git a/client/css/app.css b/client/css/app.css index 5e46ab5..6e753aa 100644 --- a/client/css/app.css +++ b/client/css/app.css @@ -397,8 +397,10 @@ input { } table.parameter-list { + border-spacing: 0; text-align: left; & th { + padding: 0; padding-right: 1em; } } diff --git a/client/store.js b/client/store.js index 5ef7948..8519c37 100644 --- a/client/store.js +++ b/client/store.js @@ -400,6 +400,12 @@ function logOut() { }); } +function ping(host) { + return m.request({ + url: `/api/ping/${encodeURIComponent(host)}` + }); +} + export { count, fetch, @@ -411,5 +417,6 @@ export { deleteResource, evaluateExpression, logIn, - logOut + logOut, + ping }; diff --git a/config.json b/config.json index 685fbf0..31bfa93 100644 --- a/config.json +++ b/config.json @@ -59,6 +59,7 @@ "ui.overview.charts.online.slices.others.color": "#f7fcb9", "ui.overview.charts.online.slices.others.filter": "Events.Inform < (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000)", "ui.device.10.type": "tags", + "ui.device.25.type": "ping", "ui.device.30.type": "parameter-list", "ui.device.30.parameters.1.type": "container", "ui.device.30.parameters.1.label": "Last inform", diff --git a/server/api-functions.js b/server/api-functions.js index 78a8bb2..8353690 100644 --- a/server/api-functions.js +++ b/server/api-functions.js @@ -367,9 +367,65 @@ function updateTags(deviceId, tags) { ); } +function ping(host) { + return new Promise((resolve, reject) => { + const options = url.parse( + `${config.get("server.nbi")}ping/${encodeURIComponent(host)}` + ); + + let _http = options.protocol === "https:" ? https : http; + _http + .request(options, res => { + if (res.statusCode === 404) { + res.resume(); + return resolve({}); + } + + if (res.statusCode !== 200) { + res.resume(); + return reject(new Error(`Unexpected status code ${res.statusCode}`)); + } + + const chunks = []; + let bytes = 0; + res.on("data", chunk => { + chunks.push(chunk); + bytes += chunk.length; + }); + + res.on("end", () => { + let buf = new Buffer(bytes); + let o = 0; + for (let c of chunks) { + c.copy(buf, o, 0, c.length); + o += c.length; + } + let m = buf + .toString() + .match( + /(\d) packets transmitted, (\d) received, ([\d.%]+) packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/ + ); + if (!m) return reject(new Error("Could not parse ping response")); + + resolve({ + packetsTransmitted: +m[1], + packetsReceived: +m[2], + packetLoss: m[3], + min: +m[4], + avg: +m[5], + max: +m[6], + mdev: +m[7] + }); + }); + }) + .end(); + }); +} + exports.query = query; exports.count = count; exports.postTasks = postTasks; exports.deleteResource = deleteResource; exports.updateTags = updateTags; exports.filterToMongoQuery = filterToMongoQuery; +exports.ping = ping; diff --git a/server/api.js b/server/api.js index cf47d7d..89a72a2 100644 --- a/server/api.js +++ b/server/api.js @@ -137,4 +137,8 @@ router.post("/devices/:id/tags", async (ctx, next) => { ctx.body = ""; }); +router.get("/ping/:host", async ctx => { + ctx.body = await apiFunctions.ping(ctx.params.host); +}); + module.exports = router;