Once initialized, this script runs in the background and essentially acts as its own "plugin." It connects to the "secret" G HUB WebSockets server and reports battery status for each device GH shows as having a battery.
The script is also set up to send battery status icons based on the current charge level and charging status (to be used as a Touch Portal button background). Two example image sets are provided (horizontal and vertical battery orientation), custom sets could be substituted, or the image feature can be disabled altogether.
The script can be configured to automatically retry connecting to G HUB, for example if G HUB is not running when the script starts or is stopped/re-started later. This is described in more detail below.
Here is a brief rundown of what's on the page.
The script cannot connect to G HUB when GH isn't running, and it will be disconnected if G HUB stops running for whatever reason (an update, crash, manual restart, etc).
If the auto-connect feature is disabled or exceeds the maximum number of retries, the CONNECT button can be used to attempt a connection manually once you know G HUB is running. If auto-connect is enabled, this also resets the current retry count.
Some (all?) battery-powered devices will eventually go to sleep if unused for some period of time, and/or can be turned off with a switch on the device. G HUB treats these as "not active" devices, meaning they're not entirely disconnected, but their actual status is unknown. Similarly, if we get disconnected from G HUB, we don't know a device's status either. Presumably, we want to know when this happens so we're not seeing a happy green "all good" battery icon when in fact we don't know what the charge level is.
If G HUB gets disconnected (for whatever reason) or the whole Dynamic Script Engine plugin is stopped, then all monitored devices are set to "inactive" state.
By default, when a device becomes inactive the script will also reset all of it's battery data to default "unknown" values (essentially like G HUB does). The charge level and remaining run time state values are set to -1 and the battery image (if used) changes to the "inactive" variant.
If you want to keep the last known battery details, the script has two other modes of operation which can be configured using the SET INACTIVITY BEHAVIOR button (edit it and enable/activate the action which sets one of the modes). The three modes are:
If you connect a new battery-powered G HUB device to your computer after the script has already started and connected, G HUB should report this to the script and the new device will be added (or updated) automatically. Removing a device from the system should act the same as if it went "inactive."
Here's a screenshot of what a set of states created for a monitored device looks like and where to find them. The device name will vary of course, and you may have multiple devices. This example is from the "control panel" page. (Click for larger version).
Everything the script does is logged to the plugin's log files. If there are any errors or other unexpected issues, they should be noted in the logs. Normal activity like connection/disconnection or device discovery events are also recorded.
   35  SCRIPT_NAME = 
"G HUB Monitor",
 
   40  GHUB_WS_URL = 
"ws://localhost:9010",
 
   44  LOG_PREFIX = SCRIPT_NAME + 
":" 
   60export 
const Settings =
 
   66  ConnectRetryTimeSec: 30,
 
   72  ConnectRetryMaxCount: 20,
 
   83  DeviceInactiveAction: 
'reset',
 
   89  ImagesPath: DEFAULT_IMAGES_PATH,
 
   94  ImagePrefix: 
"h-battery-",
 
  106export 
function connect()
 
  108  _data.reconnectCount = 0;
 
  115export 
function disconnect() {
 
  127export 
function listDevices()
 
  132      path: 
"/devices/list" 
  139export 
function isConnected() {
 
  155export 
function setOption(name, value)
 
  157  if (name in Settings && typeof Settings[name] == typeof value && Settings[name] !== value) {
 
  158    Settings[name] = value;
 
  159    console.info(LOG_PREFIX, `Set option 
'${name}' to 
'${value}'`);
 
  168export 
function saveSettings() {
 
  169  _data.instance.dataStore.Settings = structuredClone(Settings);
 
  170  console.info(LOG_PREFIX, 
"Saved settings.");
 
  185function batteryLevelToImageName(level, charging = 
false)
 
  189  const chrgSfx = charging ? 
"-charging" : 
"";
 
  191    return "0" + chrgSfx;
 
  193    return "25" + chrgSfx;
 
  195    return "50" + chrgSfx;
 
  197    return "75" + chrgSfx;
 
  198  return "100" + chrgSfx;
 
  208  constructor(init = {})
 
  225    if (init?.battery != undefined) {
 
  226      if (typeof init.battery == 
'object')
 
  227        Object.assign(this.battery, init.battery);
 
  230    Object.assign(
this, init);
 
  242    if (active == this.active)
 
  244    this.active = active;
 
  248    subscribeDeviceBatteryState(
this, active);
 
  252      getDeviceBatteryState(
this);
 
  255    else if (Settings.DeviceInactiveAction == 
'reset') {
 
  259    else if (Settings.DeviceInactiveAction == 
'resetImage') {
 
  269    this.battery.percentage = -1;
 
  270    this.battery.charging = 
false;
 
  271    if (this.battery.mileage != 
null)
 
  272      this.battery.mileage = -1;
 
  280    const stateId = this.stateId,
 
  282      parentGroup = `G HUB Device - ${name}`;
 
  284    if (!stateId || !name)
 
  288    TP.
stateCreate(`${stateId}.level`,    parentGroup, `${name} Battery Level`, 
"-1");
 
  289    TP.
stateCreate(`${stateId}.mileage`,  parentGroup, `${name} Battery Hours Remaining`, 
"-1");
 
  290    TP.
stateCreate(`${stateId}.name`,     parentGroup, `${name} Device Name`, name);
 
  291    TP.
stateCreate(`${stateId}.active`,   parentGroup, `${name} Is Active`, 
"0");
 
  292    TP.
stateCreate(`${stateId}.charging`, parentGroup, `${name} Is Charging`, 
"0");
 
  293    if (Settings.ImagesPath) {
 
  295      TP.
stateCreate(`${stateId}.image`,  parentGroup, `${name} Battery Image`, 
"");
 
  310    const b = this.battery;
 
  313    if (b.mileage != 
null)
 
  330    if (!Settings.ImagesPath || 
this.lastImage == imgName)
 
  333    this.lastImage = imgName;
 
  334    const imgPath = `${Settings.ImagesPath}/${Settings.ImagePrefix}${imgName}${Settings.ImageSuffix}`;
 
  339      catch (ex) { console.exception(ex); }
 
  345    .catch(console.exception);
 
  360function newDevice(devInfo)
 
  362  let name = devInfo.displayName;
 
  365  Array.from(_data.devices.values()).forEach((d) => { 
if (d.name.startsWith(name)) ++count; } )
 
  367    name += ` (${count})`;
 
  371    fullName: devInfo.extendedDisplayName,
 
  372    type: devInfo.deviceType,
 
  373    stateId: `${STATES_BASE_ID}.${name}`,
 
  375  console.info(LOG_PREFIX, `Added 
new device 
'${d.fullName}' of type ${d.type}`);
 
  384function parseDeviceList(deviceInfos)
 
  386  if (Array.isArray(deviceInfos)) {
 
  387    for (
const dev of deviceInfos)
 
  397function parseDevice(devInfo)
 
  400    console.error(LOG_PREFIX, 
"Missing device data in device info message.");
 
  404  if (!devInfo.capabilities?.hasBatteryStatus) {
 
  405    console.info(LOG_PREFIX, `Skipping device 
'${devInfo.displayName}' because GHUB says it doesn
't have a battery.`); 
  408  // console.debugf("Processing device info message: %1O", devInfo); 
  410  let d = _data.devices.get(devInfo.id); 
  412    _data.devices.set(devInfo.id, (d = newDevice(devInfo))); 
  414  d.setActive(devInfo.state == "ACTIVE" || devInfo.state == "PRESENT"); 
  420function parseBatteryState(state)
 
  422  if (!state || !state.deviceId) {
 
  423    console.error(LOG_PREFIX, "Missing device data in battery status message.");
 
  426  const d = _data.devices.get(state.deviceId);
 
  428    console.error(LOG_PREFIX, "Unknown Device ID in battery status message:", state.deviceId);
 
  431  // console.debugf("Processing device battery status message: %2O", payload);
 
  433  // update device battery data
 
  434  d.battery.percentage = state.percentage;
 
  435  d.battery.charging = state.charging;
 
  437  // Apparently not all devices support "mileage" estimates...
 
  438  // if we already set the mileage in a previous update then it will be non-null,
 
  439  // otherwise check the payload's `batteryMileageSupport` property value
 
  440  if (d.battery.mileage != 
null || state.batteryMileageSupport == 
"MILEAGE_SUPPORTED")
 
  441    d.battery.mileage = state.mileage;
 
  444  d.battery.lastUpdate = 
Date.now();
 
  446  d.updateBatteryStates();
 
  457function deactivateAllDevices()
 
  459  for (
const d of _data.devices.values())
 
  466function updateGHubStatus(active) {
 
  469    deactivateAllDevices();
 
  480function open(cb = 
null)
 
  485    console.info(LOG_PREFIX, 
"Connecting to G HUB @", GHUB_WS_URL);
 
  486    cancelConnectionAttempt();
 
  500function attempConnection() {
 
  501  _data.reconnectTimerId = 
null;
 
  506function cancelConnectionAttempt() {
 
  507  if (_data.reconnectTimerId) {
 
  508    clearTimeout(_data.reconnectTimerId);
 
  509    _data.reconnectTimerId = 
null;
 
  515function scheduleConnectionAttempt() {
 
  516  cancelConnectionAttempt();
 
  517  if (Settings.ConnectRetryTimeSec > 0 && (Settings.ConnectRetryMaxCount <= 0 || _data.reconnectCount <= Settings.ConnectRetryMaxCount)) {
 
  518    ++_data.reconnectCount;
 
  519    _data.reconnectTimerId = setTimeout(attempConnection, Settings.ConnectRetryTimeSec * 1000);
 
  520    console.info(LOG_PREFIX, `Attempting to re-connect in ${Settings.ConnectRetryTimeSec} seconds...`);
 
  525function onAboutToQuit() {
 
  526  _data.aboutToQuit = 
true;
 
  539function send(message, connectIfClosed = 
true)
 
  543  if (!(
'verb' in message) || !(
'path' in message))
 
  544    throw new TypeError(
"'verb' and 'path' message properties are required for send(message)");
 
  546  if (!isConnected()) {
 
  548      open(() => send(message));
 
  550      console.error(LOG_PREFIX, 
"WebSocket connection is closed.");
 
  553  if (!(
'msgId' in message))
 
  555  _data.ws.send(JSON.stringify(message));
 
  562function subscribeDevices() {
 
  565    path: 
"/devices/state/changed" 
  573function getDeviceBatteryState(d) {
 
  578    path: `/battery/${d.id}/state`
 
  587function subscribeDeviceBatteryState(d, sub = 
true) {
 
  588  if (!d?.
id || !isConnected())
 
  591    verb: sub ? 
"SUBSCRIBE" : 
"UNSUBSCRIBE",
 
  592    path: `/battery/${d.id}/state/changed`
 
  598function subscribeBatteryState(sub = 
true) {
 
  600    verb: sub ? 
"SUBSCRIBE" : 
"UNSUBSCRIBE",
 
  601    path: `/battery/state/changed`
 
  610  console.info(LOG_PREFIX, 
'connected to G HUB.');
 
  611  _data.reconnectCount = 0;
 
  619function onWsClosed(event) {
 
  620  console.info(LOG_PREFIX, 
'disconnected.');
 
  626function onWsError(event) {
 
  627  console.error(LOG_PREFIX, 
'WebSocket Error:', event, 
"code:", event.code);
 
  629  if (event.code == 
Socket.ConnectionRefusedError || event.code == 
Socket.RemoteHostClosedError)
 
  630    scheduleConnectionAttempt();
 
  634function onWsMessage(event)
 
  637    const msg = JSON.parse(event.data);
 
  639      console.warn(LOG_PREFIX, 
"Message is missing 'verb' property:", event.data);
 
  642    if (msg.verb != 
'BROADCAST' && msg.verb != 
'GET') {
 
  643      console.info(LOG_PREFIX, 
"Skipping un-requested message type:", msg.verb);
 
  646    if (msg.result && msg.result.code != 
"SUCCESS") {
 
  647      console.warn(LOG_PREFIX, 
"Got error response from G HUB:", event.data);
 
  651      console.warn(LOG_PREFIX, 
"G HUB message is missing payload:", event.data);
 
  655    if (msg.path == 
'/devices/list')
 
  656      parseDeviceList(msg.payload.deviceInfos);
 
  657    else if (msg.path == 
'/devices/state/changed')
 
  658      parseDevice(msg.payload);
 
  659    else if (msg.path.match(/battery\/.*?\/?state/))
 
  660      parseBatteryState(msg.payload);
 
  662      console.warn(`Received unknown message with path:`, msg.path);
 
  665    console.errorf(
"%s Error decoding message data: %s\nMessage data was:\n%2O", LOG_PREFIX, ex, event.data);
 
  681  console.info(LOG_PREFIX, 
"initializing...");
 
  685  if (!_data.instance) {
 
  686    console.error(LOG_PREFIX, 
"Something went wrong, could not find current DynamicScript instance!");
 
  692  const ds = _data.instance.dataStore;
 
  694    console.info(LOG_PREFIX, 
"Restoring saved settings");
 
  695    Object.assign(Settings, ds.Settings);
 
  698    console.info(LOG_PREFIX, 
"No saved settings found, creating new data store");
 
  699    ds.Settings = structuredClone(Settings);
 
  703  _data.ghubStatusStateId = `${STATES_BASE_ID}.ghub.active`;
 
  704  TP.
stateCreate(_data.ghubStatusStateId, SCRIPT_NAME, 
"G HUB Connection Status", 
"0");
 
  711  _data.ws = 
new WebSocket(GHUB_WS_URL, 
"json", { origin: 
"file://" });
 
  713  _data.ws.closed.connect(onWsClosed);
 
  714  _data.ws.error.connect(onWsError);
 
  715  _data.ws.message.connect(onWsMessage);
 
  716  _data.ws.opened.connect(onWsOpened);
 
  718  console.info(LOG_PREFIX, 
"initialization completed.");
 
  728  reconnectTimerId: 
null,             
 
  730  ghubStatusStateId: 
"",              
 
The DSE object contains constants and functions related to the plugin environment....
Definition: DSE.h:50
 
void aboutToQuit()
This event is emitted just before the plugin process exits, but allows one cycle for event handlers t...
 
DynamicScript * currentInstance()
Returns the current script Instance. This is equivalent to calling DSE.instance(DSE....
Definition: DSE.h:279
 
string currentInstanceName
The Instance Name of the script currently being evaluated, as specified in the corresponding Touch Po...
Definition: DSE.h:133
 
DynamicScript instance(String name)
Returns the script instance with given name, if any, otherwise returns null.
 
string VALUE_STATE_PREFIX
The prefix added by the plugin to an script instance's Name to form a unique State ID,...
Definition: DSE.h:73
 
string TP_USER_DATA_PATH
Contains the value of the current system user's Touch Portal settings directory path....
Definition: DSE.h:98
 
Date prototype extension methods
 
The Dir class provides static functions for directory operations, which are accessed in JS with the D...
Definition: Dir.h:43
 
string normalize(string &path)
Returns the canonical path, i.e. a path without symbolic links or redundant "." or "....
Definition: Dir.h:130
 
The FS namespace holds constants related to file system tasks. They are referenced in JavaScript as F...
Definition: FS.h:17
 
@ O_BIN
("b") Handle file in binary mode (returns results as byte arrays).
Definition: FS.h:40
 
The File class provides access to... files!
Definition: File.dox:412
 
< Promise|void > readAsync(string file,< Options|string|FS.OpenMode|Function|undefined > options,< Function|undefined > callback)
Read the contents of a file asynchronously using a callback or Promise to handle the results.
 
Instances of the GHUBDevice class are used to track detected GHUB devices.
Definition: ghub-monitor.mjs:207
 
updateBatteryStateImage(imgName, synchronous=_data.aboutToQuit)
Sends a TP state update for this device with image data read from imgName.
Definition: ghub-monitor.mjs:327
 
createStates()
Creates all TP States for this device.
Definition: ghub-monitor.mjs:278
 
resetBatteryData(withUpdate=false)
Resets battery data to default "unknown" values and optionally sends TP state updates for this device...
Definition: ghub-monitor.mjs:267
 
updateBatteryStates()
Sends TP battery state updates for this device.
Definition: ghub-monitor.mjs:305
 
setActive(active)
Sets the active state of this device.
Definition: ghub-monitor.mjs:240
 
updateImageState(img)
Definition: ghub-monitor.mjs:349
 
void callLater(Function function, any ... arguments)
Use this function to eliminate redundant calls to a function.
 
The global TP namespace is used as a container for all Touch Portal API methods, which are invoked wi...
Definition: touchportal.dox:6
 
void stateUpdateById(string id, string value)
Send a State update to Touch Portal for any arbitrary state/value matching the id,...
 
void stateCreate(string id, string parentGroup, string description, string defaultValue="", boolean forceUpdate=false, int delayMs=0)
Create a new Touch Portal dynamic State (if it doesn't already exist).
 
Implementation of the WebSocket Web API
Definition: WebSocket.h:128
 
@ CONNECTING
(0) Socket has been created but the connection is not yet open.
Definition: WebSocket.h:209
 
@ OPEN
(1) The connection is open and ready to communicate.
Definition: WebSocket.h:210
 
@ CLOSING
(2) The connection is in the process of closing.
Definition: WebSocket.h:211
 
The Socket namespace contains constants relating to network socket operations.
Definition: fetch.dox:575
 
 Logitech, Logi, G HUB, and their logos are trademarks or registered trademarks of Logitech Europe S.A. and/or its affiliates in the United States and/or other countries.