homebridge-plugin-utils
Version:
Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.
202 lines • 7.13 kB
JavaScript
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
*
* util.ts: Useful utility functions when writing TypeScript.
*/
/**
* A utility method that formats a bitrate value into a human-readable form as kbps or Mbps.
*
* @param value - The bitrate value to convert.
*
* @returns Returns the value as a human-readable string.
* @example
*
* ```ts
* formatBps(500); // "500 bps".
* formatBps(2000); // "2.0 kbps".
* formatBps(15000); // "15.0 kbps".
* formatBps(1000000); // "1.0 Mbps".
* formatBps(2560000); // "2.6 Mbps".
* ```
*/
export function formatBps(value) {
// Return the bitrate as-is.
if (value < 1000) {
return value.toString() + " bps";
}
// Return the bitrate in kilobits.
if (value < 1000000) {
const kbps = value / 1000;
return ((kbps % 1) === 0 ? kbps.toFixed(0) : kbps.toFixed(1)) + " kbps";
}
// Return the bitrate in megabits.
const mbps = value / 1000000;
return ((mbps % 1) === 0 ? mbps.toFixed(0) : mbps.toFixed(1)) + " Mbps";
}
/**
* A utility method that retries an operation at a specific interval for up to an absolute total number of retries.
*
* @param operation - The operation callback to try until successful.
* @param retryInterval - Interval to retry, in milliseconds.
* @param totalRetries - Optionally, specify the total number of retries.
*
* @returns Returns `true` when the operation is successful, `false` otherwise or if the total number of retries has been exceeded.
*
* @remarks `operation` must be an asynchronous function that returns `true` when successful, and `false` otherwise.
*
* @example
* ```ts
* // Example: Retry an async operation up to 5 times, waiting 1 second between each try.
* let attempt = 0;
* const result = await retry(async () => {
*
* attempt++;
*
* // Simulate a 50% chance of success
* return Math.random() > 0.5 || attempt === 5;
* }, 1000, 5);
*
* console.log(result); // true if operation succeeded within 5 tries, otherwise false.
* ```
*
* @category Utilities
*/
export async function retry(operation, retryInterval, totalRetries) {
if ((totalRetries !== undefined) && (totalRetries <= 0)) {
return false;
}
// Try the operation that was requested.
if (!(await operation())) {
// If the operation wasn't successful, let's sleep for the requested interval and try again.
await sleep(retryInterval);
return retry(operation, retryInterval, (totalRetries === undefined) ? undefined : --totalRetries);
}
// We were successful - we're done.
return true;
}
/**
* Run a promise with a guaranteed timeout to complete.
*
* @typeParam T - The type of value the promise resolves with.
* @param promise - The promise you want to run.
* @param timeout - The amount of time, in milliseconds, to wait for the promise to resolve.
*
* @returns Returns the result of resolving the promise it's been passed if it completes before timeout, or null if the timeout expires.
*
* @example
* ```ts
* // Resolves in 100ms, timeout is 500ms, so it resolves to 42.
* const result = await runWithTimeout(Promise.resolve(42), 500);
* console.log(result); // 42
*
* // Resolves in 1000ms, timeout is 500ms, so it resolves to null.
* const slowPromise = new Promise<number>(resolve => setTimeout(() => resolve(42), 1000));
* const result2 = await runWithTimeout(slowPromise, 500);
* console.log(result2); // null
* ```
*
* @category Utilities
*/
export async function runWithTimeout(promise, timeout) {
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), timeout));
return Promise.race([promise, timeoutPromise]);
}
/**
* Emulate a sleep function.
*
* @param sleepTimer - The amount of time to sleep, in milliseconds.
*
* @returns Returns a promise that resolves after the specified time elapses.
*
* @example
* To sleep for 3 seconds before continuing execute:
*
* ```ts
* await sleep(3000)
* ```
*
* @category Utilities
*/
export async function sleep(sleepTimer) {
return new Promise(resolve => setTimeout(resolve, sleepTimer));
}
/**
* Camel case a string.
*
* @param input - The string to camel case.
*
* @returns Returns the camel cased string.
*
* @example
* ```ts
* toCamelCase(This is a test)
* ```
*
* Returns: `This Is A Test`, capitalizing the first letter of each word.
* @category Utilities
*/
export function toCamelCase(input) {
return input.replace(/(^\w|\s+\w)/g, match => match.toUpperCase());
}
/**
* Sanitize an accessory name according to HomeKit naming conventions.
*
* @param name - The name to validate.
*
* @returns Returns the HomeKit-sanitized version of the name, replacing invalid characters with a space and squashing multiple spaces.
*
* @remarks This sanitizes names using [HomeKit's naming rulesets](https://developer.apple.com/design/human-interface-guidelines/homekit#Help-people-choose-useful-names)
* and HAP specification documentation:
*
* - Starts and ends with a letter or number. Exception: may end with a period.
* - May have the following special characters: -"',.#&.
* - Must not include emojis.
*
* @example
* ```ts
* sanitizeName("Test|Switch")
* ```ts
*
* Returns: `Test Switch`, replacing the pipe (an invalid character in HomeKit's naming ruleset) with a space.
*
* @category Utilities
*/
export function sanitizeName(name) {
// Here are the steps we're taking to sanitize names for HomeKit:
//
// - Replace any disallowed char (including emojis) with a space.
// - Collapse multiple spaces to one.
// - Trim spaces at the beginning and end of the string.
// - Strip any leading non-letter/number.
// - Collapse two or more trailing periods into one.
// - Remove any other trailing char that's not letter/number/period.
return name.replace(/[^\p{L}\p{N}\-"'.,#&\s]/gu, " ").replace(/\s+/g, " ").trim().replace(/^[^\p{L}\p{N}]+/u, "").replace(/\.{2,}$/g, ".").
replace(/[^\p{L}\p{N}.]$/u, "");
}
/**
* Validate an accessory name according to HomeKit naming conventions.
*
* @param name - The name to validate.
*
* @returns Returns `true` if the name passes HomeKit's naming rules, `false` otherwise.
*
* @remarks This validates names using [HomeKit's naming rulesets](https://developer.apple.com/design/human-interface-guidelines/homekit#Help-people-choose-useful-names)
* and HAP specification documentation:
*
* - Starts and ends with a letter or number. Exception: may end with a period.
* - May not have multiple spaces adjacent to each other, nor begin nor end with a space.
* - May have the following special characters: -"',.#&.
* - Must not include emojis.
*
* @example
* ```ts
* validateName("Test|Switch")
* ```ts
*
* Returns: `false`.
*
* @category Utilities
*/
export function validateName(name) {
return /^(?!.*\p{Extended_Pictographic})(?!.* {2})(?=^[\p{L}\p{N}].*[\p{L}\p{N}.]$)[\p{L}\p{N}\-"'.,#& ]+$/u.test(name);
}
//# sourceMappingURL=util.js.map