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.