/** * Synology SRM Node (child of the SRM Router device) * * One per mesh node (e.g. RT6600ax primary, RT2600ac node). Shows the node's online status as * presence plus per-node health (CPU, memory, uptime, model, firmware). * * Created automatically by the "Synology SRM Router" device — do not create manually. * * Author: Derek Osborn * Version: 1.3.1 (2026-07-08) - Added a Refresh command. Reboot logic lives in the app. */ metadata { definition(name: "Synology SRM Node", namespace: "dJOS", author: "Derek Osborn", importUrl: "https://raw.githubusercontent.com/dJOS1475/Synology-SRM-Integration/refs/heads/main/drivers/SynologySRMNode.groovy") { capability "PresenceSensor" capability "Sensor" capability "Actuator" // exposes custom commands (reboot) to Rule Machine / automations capability "Refresh" attribute "model", "string" attribute "firmware", "string" attribute "cpu", "number" // % attribute "memory", "number" // % attribute "uptime", "string" attribute "role", "string" // primary | node attribute "nodeId", "number" attribute "connectedDevices", "number" attribute "nodeStatus", "string" // good | ... attribute "connectionType", "string" // wired | wireless attribute "portsUp", "number" // ethernet ports with link attribute "lastUpdate", "string" attribute "htmlTile", "string" // dashboard summary tile command "reboot" } preferences { input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true } } def installed() { sendEvent(name: "presence", value: "not present") } def updated() { } // Reboot just this mesh node (gated by the app's "Allow Reboot" setting). def reboot() { parent?.rebootNode(device.currentValue("nodeId")) } // Refresh via the Router device. Calls refreshHealth() — NOT refresh() — because this driver defines // its own refresh() and a same-named parent call silently fails to dispatch on Hubitat. def refresh() { parent?.refreshHealth() } // Called by the parent SRM Router device with a merged per-node health map. def setNode(Map n) { String presence = (n.online ? "present" : "not present") if (device.currentValue("presence") != presence) { sendEvent(name: "presence", value: presence, descriptionText: "${device.displayName} is ${presence}") if (txtEnable) log.info "${device.displayName} is ${presence}" } updateAttr("model", n.model) updateAttr("firmware", n.firmware) updateAttr("cpu", n.cpu) updateAttr("memory", n.memory) updateAttr("uptime", n.uptime) updateAttr("role", n.role) updateAttr("nodeId", n.nodeId) updateAttr("connectedDevices", n.connectedDevices) updateAttr("nodeStatus", n.nodeStatus) updateAttr("connectionType", n.connectionType) updateAttr("portsUp", n.portsUp) updateAttr("lastUpdate", new Date().format("yyyy-MM-dd HH:mm:ss")) updateTile() } def setOffline() { if (device.currentValue("presence") != "not present") { sendEvent(name: "presence", value: "not present", descriptionText: "${device.displayName} is not present") if (txtEnable) log.info "${device.displayName} is not present" } updateTile() } private updateAttr(String name, value) { if (value != null && device.currentValue(name)?.toString() != value.toString()) { sendEvent(name: name, value: value) } } /* ---- dashboard HTML tile ---- */ private void updateTile() { boolean online = device.currentValue("presence") == "present" StringBuilder h = new StringBuilder() h << "