UNPKG

homebridge-plugin-utils

Version:

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

315 lines (314 loc) 9.9 kB
/** * TypeScript Utilities. * * @module */ /** * A utility type that recursively makes all properties of an object, including nested objects, optional. * * This should only be used on JSON objects. If used on classes, class methods will also be marked as optional. * * @remarks Credit for this type goes to: https://github.com/joonhocho/tsdef. * * @typeParam T - The type to make recursively partial. * * @example * * ```ts * type Original = { * * id: string; * nested: { value: number }; * }; * * // All properties, including nested ones, are optional. * type PartialObj = DeepPartial<Original>; * * const obj: PartialObj = { nested: {} }; * ``` * * @category Utilities */ export type DeepPartial<T> = { [P in keyof T]?: T[P] extends Array<infer I> ? Array<DeepPartial<I>> : DeepPartial<T[P]>; }; /** * A utility type that recursively makes all properties of an object, including nested objects, readonly. * * This should only be used on JSON objects. If used on classes, class methods will also be marked as readonly. * * @remarks Credit for this type goes to: https://github.com/joonhocho/tsdef. * * @typeParam T - The type to make recursively readonly. * * @example * * ```ts * type Original = { * id: string; * nested: { value: number }; * }; * * // All properties, including nested ones, are readonly. * type ReadonlyObj = DeepReadonly<Original>; * * const obj: ReadonlyObj = { id: "a", nested: { value: 1 } }; * // obj.id = "b"; // Error: cannot assign to readonly property. * ``` * * @category Utilities */ export type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends Array<infer I> ? Array<DeepReadonly<I>> : DeepReadonly<T[P]>; }; /** * Utility type that allows a value to be either the given type or `null`. * * This type is used to explicitly indicate that a variable, property, or return value may be either a specific type or `null`. * * @typeParam T - The type to make nullable. * * @example * * ```ts * let id: Nullable<string> = null; * * // Later... * id = "device-001"; * ``` * * @category Utilities */ export type Nullable<T> = T | null; /** * Makes all properties in `T` optional except for `id`, which remains required. * * @template T - The base interface or type. * * @example * * ```ts * interface Device { * * id: string; * name: string; * mac: string; * } * * type UserUpdate = PartialWithId<User>; * * // Valid: Only 'id' is required, others are optional. * const update: DeviceUpdate = { id: "123" }; * * // Valid: Extra properties can be provided. * const another: DeviceUpdate = { id: "456", name: "SomeDevice" }; * * // Error: 'id' is missing. * const error: DeviceUpdate = { name: "SomeOtherDevice" }; // TypeScript error * ``` * * @category Utilities */ export type PartialWithId<T, K extends keyof T> = Partial<T> & Pick<T, K>; /** * Logging interface for Homebridge plugins. * * This interface defines the standard logging methods (`debug`, `info`, `warn`, `error`) that plugins should use to output log messages at different severity levels. It * is intended to be compatible with Homebridge's builtin logger and can be implemented by any custom logger used within Homebridge plugins. * * @example * * ```ts * function example(log: HomebridgePluginLogging) { * * log.debug("Debug message: %s", "details"); * log.info("Informational message."); * log.warn("Warning message!"); * log.error("Error message: %s", "problem"); * } * ``` * * @category Utilities */ export interface HomebridgePluginLogging { /** * Logs a debug-level message. * * @param message - The message string, with optional format specifiers. * @param parameters - Optional parameters for message formatting. */ debug: (message: string, ...parameters: unknown[]) => void; /** * Logs an error-level message. * * @param message - The message string, with optional format specifiers. * @param parameters - Optional parameters for message formatting. */ error: (message: string, ...parameters: unknown[]) => void; /** * Logs an info-level message. * * @param message - The message string, with optional format specifiers. * @param parameters - Optional parameters for message formatting. */ info: (message: string, ...parameters: unknown[]) => void; /** * Logs a warning-level message. * * @param message - The message string, with optional format specifiers. * @param parameters - Optional parameters for message formatting. */ warn: (message: string, ...parameters: unknown[]) => void; } /** * 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 declare function formatBps(value: number): string; /** * 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 declare function retry(operation: () => Promise<boolean>, retryInterval: number, totalRetries?: number): Promise<boolean>; /** * 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 declare function runWithTimeout<T>(promise: Promise<T>, timeout: number): Promise<Nullable<T>>; /** * 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 declare function sleep(sleepTimer: number): Promise<NodeJS.Timeout>; /** * 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 declare function toCamelCase(input: string): string; /** * 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 declare function sanitizeName(name: string): string; /** * 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 declare function validateName(name: string): boolean;