UNPKG

homebridge-plugin-utils

Version:

Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.

202 lines 7.13 kB
/* 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