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
/**
* 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;