diff --git a/dist/esm/ActionMapping.d.ts b/dist/esm/ActionMapping.d.ts new file mode 100644 index 0000000..3f1fd42 --- /dev/null +++ b/dist/esm/ActionMapping.d.ts @@ -0,0 +1,20 @@ +export declare enum AlexaActions { + Open = "Alexa.Actions.Open", + Close = "Alexa.Actions.Close", + Raise = "Alexa.Actions.Raise", + Lower = "Alexa.Actions.Lower", + SetEcoOn = "Alexa.Actions.SetEcoOn", + SetEcoOff = "Alexa.Actions.SetEcoOff" +} +interface Directive { + name: string; + payload?: string; +} +export declare class ActionMapping { + type: string; + actions: AlexaActions[]; + directive: Directive; + constructor(actions: AlexaActions[], directiveName: string, directivePayload?: string); + toJSON(): object; +} +export {}; diff --git a/dist/esm/Alex2Node.d.ts b/dist/esm/Alex2Node.d.ts new file mode 100644 index 0000000..48cce6d --- /dev/null +++ b/dist/esm/Alex2Node.d.ts @@ -0,0 +1,59 @@ +import type { IClientOptions } from "mqtt"; +import Device from "./Device.js"; +import { EventEmitter } from "events"; +import { DisplayCategory } from "./DisplayCategory.js"; +/** Optional settings for the bridge (1.5.1). Everything has the 1.4.0 behaviour as its default. */ +export interface Alex2MQTTOptions { + /** The broker URL. Default: the public Alex2MQTT broker, mqtt://Alex2MQTT.stormysdream.club:1883. */ + host?: string; + /** Extra mqtt.js client options (reconnectPeriod, connectTimeout, clientId, ...). Merged over the defaults. */ + mqtt?: IClientOptions; + /** Where log lines go. Default: console (only when debugLogging is on). */ + log?: (message: string, detail?: unknown) => void; +} +export declare const DEFAULT_HOST = "mqtt://Alex2MQTT.stormysdream.club:1883"; +/** + * The Alexa-to-MQTT bridge: one broker connection for a user's root topic, a set of registered devices, discovery and + * directive dispatch. + * + * Events (all optional to listen to - since 1.5.1 a broker outage never throws out of the library): + * "connect" connected (or reconnected) and subscribed to /# + * "offline" the connection dropped; mqtt.js reconnects on its own (reconnectPeriod, default 1 s) + * "reconnect" a reconnect attempt starts + * "close" the connection closed + * "error" (err) a connection or publish error. Emitted ONLY when a listener is attached (1.4.0 emitted it + * unconditionally, and Node kills a process that has an unhandled "error" event) + * "discover" (n) a discovery request was answered with n devices + * "directive" (info) a directive was dispatched to a device: { endpointId, namespace, name } + */ +declare class Alex2MQTT extends EventEmitter { + private username; + private password; + private rootTopic; + private debugLogging; + private client; + private devices; + private MqttHost; + private options; + /** true while the broker connection is up. */ + connected: boolean; + /** ISO time of the last discovery request answered, null before the first. */ + lastDiscoveryAt: string | null; + constructor(username: string, password: string, rootTopic: string, debugLogging?: boolean, options?: Alex2MQTTOptions); + private log; + /** Emit "error" only when somebody listens: an unhandled "error" event would crash the host process. */ + private fail; + connect(): void; + /** Close the broker connection (resolves once closed). The devices stay registered; connect() again reuses them. */ + disconnect(): Promise; + registerDevice(name: string, endpointId: string, displayCategory: DisplayCategory | DisplayCategory[] | null): Device; + /** Forget a device (its listeners with it). Returns false when there was none. */ + unregisterDevice(endpointId: string): boolean; + /** Forget every device. */ + clearDevices(): void; + getDevices(): Device[]; + getDevice(endpointId: string): Device | undefined; + getRootTopic(): string; + getHost(): string; +} +export default Alex2MQTT; diff --git a/dist/esm/AlexaErrorResponse.d.ts b/dist/esm/AlexaErrorResponse.d.ts new file mode 100644 index 0000000..7620270 --- /dev/null +++ b/dist/esm/AlexaErrorResponse.d.ts @@ -0,0 +1,92 @@ +import type { MqttClient } from "mqtt"; +export declare enum AlexaErrorType { + ALREADY_IN_OPERATION = "ALREADY_IN_OPERATION", + AUTHORIZATION_REQUIRED = "AUTHORIZATION_REQUIRED", + BRIDGE_UNREACHABLE = "BRIDGE_UNREACHABLE", + BYPASS_NEEDED = "BYPASS_NEEDED", + CLOUD_CONTROL_DISABLED = "CLOUD_CONTROL_DISABLED", + CHILD_LOCK = "CHILD_LOCK", + CONFIGURATION_UPDATE_NOT_ALLOWED = "CONFIGURATION_UPDATE_NOT_ALLOWED", + COOK_DURATION_TOO_LONG = "COOK_DURATION_TOO_LONG", + COOLING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE = "COOLING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE", + COOLING_STAGES_EXCEEDS_LIMIT = "COOLING_STAGES_EXCEEDS_LIMIT", + DATA_DELETION_NOT_SUPPORTED = "DATA_DELETION_NOT_SUPPORTED", + DATA_RETRIEVAL_NOT_SUPPORTED = "DATA_RETRIEVAL_NOT_SUPPORTED", + DEVICE_STUCK = "DEVICE_STUCK", + DISABLED_BY_USER = "DISABLED_BY_USER", + DO_NOT_DISTURB_MODE = "DO_NOT_DISTURB_MODE", + DOOR_CLOSED_TOO_LONG = "DOOR_CLOSED_TOO_LONG", + DOOR_OPEN = "DOOR_OPEN", + DUAL_SETPOINTS_UNSUPPORTED = "DUAL_SETPOINTS_UNSUPPORTED", + ENDPOINT_BUSY = "ENDPOINT_BUSY", + ENDPOINT_CONTROL_UNAVAILABLE = "ENDPOINT_CONTROL_UNAVAILABLE", + ENDPOINT_LOW_POWER = "ENDPOINT_LOW_POWER", + ENDPOINT_UNREACHABLE = "ENDPOINT_UNREACHABLE", + EXCEEDED_PIN_ATTEMPTS = "EXCEEDED_PIN_ATTEMPTS", + EXPIRED_AUTHORIZATION_CREDENTIAL = "EXPIRED_AUTHORIZATION_CREDENTIAL", + FAILED_TO_BOOTSTRAP_COMMISSIONING_PROCESS = "FAILED_TO_BOOTSTRAP_COMMISSIONING_PROCESS", + FIRMWARE_OUT_OF_DATE = "FIRMWARE_OUT_OF_DATE", + HARDWARE_MALFUNCTION = "HARDWARE_MALFUNCTION", + HEATING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE = "HEATING_LOCKOUT_TEMPERATURE_VALUE_OUT_OF_RANGE", + HEATING_STAGES_EXCEEDS_LIMIT = "HEATING_STAGES_EXCEEDS_LIMIT", + INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS", + INSUFFICIENT_RESOURCE = "INSUFFICIENT_RESOURCE", + INSUFFICIENT_SPACE = "INSUFFICIENT_SPACE", + INTERNAL_ERROR = "INTERNAL_ERROR", + INVALID_AUTHORIZATION_CREDENTIAL = "INVALID_AUTHORIZATION_CREDENTIAL", + INVALID_AUXILIARY_HEATING_SYSTEM_TYPE = "INVALID_AUXILIARY_HEATING_SYSTEM_TYPE", + INVALID_DIRECTIVE = "INVALID_DIRECTIVE", + INVALID_SYSTEM_TYPE = "INVALID_SYSTEM_TYPE", + INVALID_TARGET_STATE = "INVALID_TARGET_STATE", + INVALID_TEMPERATURE_SCALE = "INVALID_TEMPERATURE_SCALE", + INVALID_TERMINAL_CONNECTION = "INVALID_TERMINAL_CONNECTION", + INVALID_VALUE = "INVALID_VALUE", + MAINTENANCE_REQUIRED = "MAINTENANCE_REQUIRED", + MAX_COMMISSIONING_LIMIT_REACHED = "MAX_COMMISSIONING_LIMIT_REACHED", + MISSING_SETUP_INFORMATION = "MISSING_SETUP_INFORMATION", + NO_SUCH_ENDPOINT = "NO_SUCH_ENDPOINT", + NOT_CALIBRATED = "NOT_CALIBRATED", + NOT_IN_OPERATION = "NOT_IN_OPERATION", + NOT_READY = "NOT_READY", + NOT_SUPPORTED_IN_CURRENT_MODE = "NOT_SUPPORTED_IN_CURRENT_MODE", + NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE = "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE", + OBSTACLE_DETECTED = "OBSTACLE_DETECTED", + PARTNER_APPLICATION_REDIRECTION = "PARTNER_APPLICATION_REDIRECTION", + PIN_SETUP_REQUIRED = "PIN_SETUP_REQUIRED", + POWER_LEVEL_NOT_SUPPORTED = "POWER_LEVEL_NOT_SUPPORTED", + PREHEAT_REQUIRED = "PREHEAT_REQUIRED", + PROBE_REQUIRED = "PROBE_REQUIRED", + RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED", + REMOTE_START_NOT_SUPPORTED = "REMOTE_START_NOT_SUPPORTED", + REMOVE_PROBE = "REMOVE_PROBE", + REMOTE_START_DISABLED = "REMOTE_START_DISABLED", + REQUESTED_SETPOINTS_TOO_CLOSE = "REQUESTED_SETPOINTS_TOO_CLOSE", + SAFETY_BEAM_BREACHED = "SAFETY_BEAM_BREACHED", + SUBSCRIPTION_REQUIRED = "SUBSCRIPTION_REQUIRED", + TEMPERATURE_VALUE_OUT_OF_RANGE = "TEMPERATURE_VALUE_OUT_OF_RANGE", + THERMOSTAT_IS_OFF = "THERMOSTAT_IS_OFF", + TOO_MANY_FAILED_ATTEMPTS = "TOO_MANY_FAILED_ATTEMPTS", + TRIPLE_SETPOINTS_UNSUPPORTED = "TRIPLE_SETPOINTS_UNSUPPORTED", + UNABLE_TO_CHARGE = "UNABLE_TO_CHARGE", + UNAUTHORIZED = "UNAUTHORIZED", + UNCLEARED_ALARM = "UNCLEARED_ALARM", + UNSUPPORTED_THERMOSTAT_MODE = "UNSUPPORTED_THERMOSTAT_MODE", + UNCLEARED_TROUBLE = "UNCLEARED_TROUBLE", + UNWILLING_TO_SET_SCHEDULE = "UNWILLING_TO_SET_SCHEDULE", + UNWILLING_TO_SET_VALUE = "UNWILLING_TO_SET_VALUE", + VALUE_OUT_OF_RANGE = "VALUE_OUT_OF_RANGE" +} +export declare class AlexaErrorResponse { + private event; + private rootTopic; + private endpointId; + private mqttClient; + /** Where a failed publish is reported (set by the Device that built this message, 1.5.2): send() never rejects. */ + onPublishError?: (err: Error) => void; + constructor(correlationToken: string, rootTopic: string, endpointId: string, mqttClient: MqttClient); + setErrorMessage(type: string, message: string, otherParams?: Record): void; + toJSON(): { + event: any; + }; + send(sendAsync?: boolean): Promise; +} diff --git a/dist/esm/AlexaInterface.d.ts b/dist/esm/AlexaInterface.d.ts new file mode 100644 index 0000000..9efe0f7 --- /dev/null +++ b/dist/esm/AlexaInterface.d.ts @@ -0,0 +1,107 @@ +import type { ActionMapping } from "./ActionMapping.js"; +export declare enum AlexaInterfaceType { + APPLICATION_STATE_REPORTER = "Alexa.ApplicationStateReporter", + AUDIO_PLAY_QUEUE = "Alexa.Audio.PlayQueue", + AUTHORIZATION_CONTROLLER = "Alexa.AuthorizationController", + AUTOMATION_MANAGEMENT = "Alexa.AutomationManagement", + AUTOMOTIVE_VEHICLE_DATA = "Alexa.Automotive.VehicleData", + BRIGHTNESS_CONTROLLER = "Alexa.BrightnessController", + CAMERA_LIVE_VIEW_CONTROLLER = "Alexa.Camera.LiveViewController", + CAMERA_STREAM_CONTROLLER = "Alexa.CameraStreamController", + CHANNEL_CONTROLLER = "Alexa.ChannelController", + COLOR_CONTROLLER = "Alexa.ColorController", + COLOR_TEMPERATURE_CONTROLLER = "Alexa.ColorTemperatureController", + COMMISSIONABLE = "Alexa.Commissionable", + CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER = "Alexa.ConsentManagement.ConsentRequiredReporter", + CONTACT_SENSOR = "Alexa.ContactSensor", + COOKING = "Alexa.Cooking", + COOKING_FOOD_TEMPERATURE_CONTROLLER = "Alexa.Cooking.FoodTemperatureController", + COOKING_FOOD_TEMPERATURE_SENSOR = "Alexa.Cooking.FoodTemperatureSensor", + COOKING_PRESET_CONTROLLER = "Alexa.Cooking.PresetController", + COOKING_TEMPERATURE_CONTROLLER = "Alexa.Cooking.TemperatureController", + COOKING_TEMPERATURE_SENSOR = "Alexa.Cooking.TemperatureSensor", + COOKING_TIME_CONTROLLER = "Alexa.Cooking.TimeController", + DATA_CONTROLLER = "Alexa.DataController", + DEVICE_USAGE_ESTIMATION = "Alexa.DeviceUsage.Estimation", + DEVICE_USAGE_METER = "Alexa.DeviceUsage.Meter", + DOORBELL_EVENT_SOURCE = "Alexa.DoorbellEventSource", + ENDPOINT_HEALTH = "Alexa.EndpointHealth", + EQUALIZER_CONTROLLER = "Alexa.EqualizerController", + INPUT_CONTROLLER = "Alexa.InputController", + INVENTORY_LEVEL_SENSOR = "Alexa.InventoryLevelSensor", + INVENTORY_LEVEL_USAGE_SENSOR = "Alexa.InventoryLevelUsageSensor", + INVENTORY_USAGE_SENSOR = "Alexa.InventoryUsageSensor", + KEYPAD_CONTROLLER = "Alexa.KeypadController", + LAUNCHER = "Alexa.Launcher", + LOCK_CONTROLLER = "Alexa.LockController", + MEDIA_PLAYBACK = "Alexa.Media.Playback", + MEDIA_PLAY_QUEUE = "Alexa.Media.PlayQueue", + MEDIA_SEARCH = "Alexa.Media.Search", + MODE_CONTROLLER = "Alexa.ModeController", + MOTION_SENSOR = "Alexa.MotionSensor", + PERCENTAGE_CONTROLLER = "Alexa.PercentageController", + PLAYBACK_CONTROLLER = "Alexa.PlaybackController", + PLAYBACK_STATE_REPORTER = "Alexa.PlaybackStateReporter", + POWER_CONTROLLER = "Alexa.PowerController", + POWER_LEVEL_CONTROLLER = "Alexa.PowerLevelController", + PROACTIVE_NOTIFICATION_SOURCE = "Alexa.ProactiveNotificationSource", + RANGE_CONTROLLER = "Alexa.RangeController", + RECORD_CONTROLLER = "Alexa.RecordController", + REMOTE_VIDEO_PLAYER = "Alexa.RemoteVideoPlayer", + RTC_SESSION_CONTROLLER = "Alexa.RTCSessionController", + SCENE_CONTROLLER = "Alexa.SceneController", + SECURITY_PANEL_CONTROLLER = "Alexa.SecurityPanelController", + SECURITY_PANEL_CONTROLLER_ALERT = "Alexa.SecurityPanelController.Alert", + SEEK_CONTROLLER = "Alexa.SeekController", + SIMPLE_EVENT_SOURCE = "Alexa.SimpleEventSource", + SMART_VISION_OBJECT_DETECTION_SENSOR = "Alexa.SmartVision.ObjectDetectionSensor", + SMART_VISION_SNAPSHOT_PROVIDER = "Alexa.SmartVision.SnapshotProvider", + SPEAKER = "Alexa.Speaker", + STEP_SPEAKER = "Alexa.StepSpeaker", + TEMPERATURE_SENSOR = "Alexa.TemperatureSensor", + THERMOSTAT_CONTROLLER = "Alexa.ThermostatController", + THERMOSTAT_CONTROLLER_CONFIGURATION = "Alexa.ThermostatController.Configuration", + THERMOSTAT_CONTROLLER_HVAC_COMPONENTS = "Alexa.ThermostatController.HVAC.Components", + THERMOSTAT_CONTROLLER_SCHEDULE = "Alexa.ThermostatController.Schedule", + TIME_HOLD_CONTROLLER = "Alexa.TimeHoldController", + TOGGLE_CONTROLLER = "Alexa.ToggleController", + UI_CONTROLLER = "Alexa.UIController", + USER_PREFERENCE = "Alexa.UserPreference", + VIDEO_RECORDER = "Alexa.VideoRecorder", + WAKE_ON_LAN_CONTROLLER = "Alexa.WakeOnLANController", + UNKNOWN = "UNKNOWN" +} +/** One ModeController mode as discovery lists it (configuration.supportedModes): the value plus its friendly names. */ +export interface SupportedMode { + value: string; + modeResources?: { + friendlyNames: Array<{ + "@type": string; + value: { + text?: string; + locale?: string; + assetId?: string; + }; + }>; + }; +} +export declare class AlexaInterface { + type: AlexaInterfaceType; + retrievable: boolean; + proactivelyReported: boolean; + instance: string; + private friendlyNames; + private actionMappings; + private supportedModes; + constructor(type: AlexaInterfaceType, retrievable?: boolean, proactivelyReported?: boolean, instance?: string); + addActionMapping(mapping: ActionMapping): void; + addFriendlyName(name: string, locale: string): void; + /** The modes of a ModeController: { value, modeResources } objects as Alexa wants them (plain strings pass through as given). */ + addSupportedModes(modes: Array): void; + setInstance(name: string): void; + getType(): AlexaInterfaceType; + getTypeString(): string; + getVersion(): string; + getProps(): string[]; + getJSON(): object; +} diff --git a/dist/esm/AlexaStatusMessage.d.ts b/dist/esm/AlexaStatusMessage.d.ts new file mode 100644 index 0000000..026845b --- /dev/null +++ b/dist/esm/AlexaStatusMessage.d.ts @@ -0,0 +1,81 @@ +import type { MqttClient } from "mqtt"; +export declare enum EndpointHealth { + OK = "OK", + UNREACHABLE = "UNREACHABLE" +} +export declare enum ThermostatMode { + OFF = "OFF", + HEAT = "HEAT", + COOL = "COOL", + AUTO = "AUTO", + ECO = "ECO", + CUSTOM = "CUSTOM" +} +export declare enum PowerController { + ON = "ON", + OFF = "OFF" +} +export declare enum TemperatureSensorScale { + CELSIUS = "CELSIUS", + FAHRENHEIT = "FAHRENHEIT" +} +interface ContextProperty { + namespace: string; + name: string; + value: any; + timeOfSample: string; + uncertaintyInMilliseconds: number; + instance?: string; +} +/** Why a ChangeReport is sent (Alexa.ChangeReport payload.change.cause.type). */ +export type ChangeCause = "APP_INTERACTION" | "PHYSICAL_INTERACTION" | "PERIODIC_POLL" | "RULE_TRIGGER" | "VOICE_INTERACTION"; +export declare class AlexaStatusMessage { + private context; + /** ChangeReport only: the properties that changed (payload.change.properties); the rest go to context. */ + private changeProps; + private changeCause; + private target; + private event; + private rootTopic; + private endpointId; + private mqttClient; + private isDeferred; + /** Where a failed publish is reported (set by the Device that built this message, 1.5.2): send() never rejects. */ + onPublishError?: (err: Error) => void; + constructor(correlationToken: string, rootTopic: string, endpointId: string, mqttClient: MqttClient, isResponse?: boolean, isDeferred?: boolean, changeCause?: ChangeCause | null); + private getTimestamp; + private addProperty; + /** ChangeReport: the add*Prop calls that follow describe what CHANGED (the default for a change report). */ + changed(): this; + /** ChangeReport: the add*Prop calls that follow describe the other, unchanged properties (context). */ + unchanged(): this; + /** True for a ChangeReport (Device.getChangeReport). */ + isChangeReport(): boolean; + /** The message as it will be published (for tests and logging). */ + toJSON(): { + event: any; + context: { + properties: ContextProperty[]; + } | null; + }; + addModeControllerProp(instance: string, value: string, uncertaintyInMs?: number): this; + addThermostatModeProp(mode: string, uncertaintyInMs?: number): this; + addEstimatedDeferralTime(seconds: number): this; + addThermostatControllerProp(name: "lowerSetpoint" | "upperSetpoint" | "targetSetpoint", scale: TemperatureSensorScale, value: number, uncertaintyInMs?: number): this; + /** Alexa.EndpointHealth connectivity: EndpointHealth.OK / UNREACHABLE (the plain strings "OK" / "UNREACHABLE" are accepted too). */ + addHealthProp(health: EndpointHealth | `${EndpointHealth}`, uncertaintyInMs?: number): this; + addPowerControllerProp(power: PowerController, uncertaintyInMs?: number): this; + addTemperatureSensorProp(scale: TemperatureSensorScale, value: number, uncertaintyInMs?: number): this; + addBrightnessControllerProp(brightness: number, uncertaintyInMs?: number): this; + addColorTemperatureControllerProp(colorTemp: number, uncertaintyInMs?: number): this; + addToggleControllerProp(state: PowerController, instance: string, uncertaintyInMs?: number): this; + addContextProp(prop: ContextProperty): this; + /** + * Publish: a Response/StateReport to //alexaResponce (sendAsync: deferredResponse), a ChangeReport + * to /changeReport (Alex2MQTT adds the user's token and posts it to the Alexa event gateway). Resolves with + * the topic, or "" when the publish failed - the error then goes to the bridge's "error" event (when listened to). + * Never rejects (1.5.1 did, so an un-caught send() could kill the host). + */ + send(sendAsync?: boolean): Promise; +} +export {}; diff --git a/dist/esm/Device.d.ts b/dist/esm/Device.d.ts new file mode 100644 index 0000000..102fb74 --- /dev/null +++ b/dist/esm/Device.d.ts @@ -0,0 +1,62 @@ +import { AlexaInterface } from "./AlexaInterface.js"; +import type { AlexaInterfaceType } from "./AlexaInterface.js"; +import { DisplayCategory } from "./DisplayCategory.js"; +import { EventEmitter } from "events"; +import type { MqttClient } from "mqtt"; +import { AlexaStatusMessage } from "./AlexaStatusMessage.js"; +import type { ChangeCause } from "./AlexaStatusMessage.js"; +import { AlexaErrorResponse } from "./AlexaErrorResponse.js"; +declare class Device extends EventEmitter { + private mqttClient; + private rootTopic; + name: string; + endpointId: string; + displayCategory: Array | null; + description: string; + manufacturerName: string; + manufacturer: string; + model: string; + protected softwareVersion: string; + private capabilities; + /** Where a failed publish from this device or a message it built is reported; Alex2MQTT sets it (1.5.2). */ + onPublishError?: (err: Error) => void; + constructor(mqttClient: MqttClient, rootTopic: string, name: string, endpointId: string, displayCategory: Array | null, description?: string, manufacturerName?: string, manufacturer?: string, model?: string); + /** Internal (1.5.2): Alex2MQTT.connect() re-binds every registered device to its new broker client after a disconnect(). */ + setMqttClient(client: MqttClient): void; + getName(): string; + setName(name: string): void; + getEndpointId(): string; + setDisplayCategory(category: DisplayCategory | DisplayCategory[]): void; + getDisplayCategory(): Array; + setDescription(description: string): void; + getDescription(): string; + getErrorMessage(correlationToken: string): AlexaErrorResponse; + getStatusMessage(correlationToken: string, isResponse?: boolean, isDeferred?: boolean): AlexaStatusMessage; + /** + * A proactive Alexa.ChangeReport (1.5.1): add the changed properties (the default target), optionally + * .unchanged() then the others, and .send() - it goes to /changeReport, which Alex2MQTT forwards to the Alexa + * event gateway with the user's token. Alexa only accepts it for capabilities registered with proactivelyReported. + */ + getChangeReport(cause?: ChangeCause): AlexaStatusMessage; + /** + * Alexa.SceneController: answer an Activate / Deactivate directive with ActivationStarted / DeactivationStarted + * (1.5.1). Resolves with the topic published to, or "" when the publish failed (never rejects, 1.5.2). + */ + sendSceneResponse(correlationToken: string, activated: boolean, cause?: ChangeCause, sendAsync?: boolean): Promise; + getCapabilities(): AlexaInterface[]; + setManufacturerName(name: string): void; + getManufacturerName(): string; + setManufacturer(manufacturer: string): void; + getManufacturer(): string; + setModel(model: string): void; + getModel(): string; + getSoftwareVersion(): string; + /** Add a capability. 1.5.1: options.retrievable / proactivelyReported / instance (a change report needs proactivelyReported). */ + addCapability(type: AlexaInterfaceType, options?: { + retrievable?: boolean; + proactivelyReported?: boolean; + instance?: string; + }): AlexaInterface; + getJSON(): Record; +} +export default Device; diff --git a/dist/esm/DisplayCategory.d.ts b/dist/esm/DisplayCategory.d.ts new file mode 100644 index 0000000..c70eeea --- /dev/null +++ b/dist/esm/DisplayCategory.d.ts @@ -0,0 +1,58 @@ +export declare enum DisplayCategory { + ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER", + AIR_CONDITIONER = "AIR_CONDITIONER", + AIR_FRESHENER = "AIR_FRESHENER", + AIR_PURIFIER = "AIR_PURIFIER", + AIR_QUALITY_MONITOR = "AIR_QUALITY_MONITOR", + ALEXA_VOICE_ENABLED = "ALEXA_VOICE_ENABLED", + AUTO_ACCESSORY = "AUTO_ACCESSORY", + BLUETOOTH_SPEAKER = "BLUETOOTH_SPEAKER", + CAMERA = "CAMERA", + CHRISTMAS_TREE = "CHRISTMAS_TREE", + COFFEE_MAKER = "COFFEE_MAKER", + COMPUTER = "COMPUTER", + CONTACT_SENSOR = "CONTACT_SENSOR", + DISHWASHER = "DISHWASHER", + DOOR = "DOOR", + DOORBELL = "DOORBELL", + DRYER = "DRYER", + EXTERIOR_BLIND = "EXTERIOR_BLIND", + FAN = "FAN", + GAME_CONSOLE = "GAME_CONSOLE", + GARAGE_DOOR = "GARAGE_DOOR", + HEADPHONES = "HEADPHONES", + HUB = "HUB", + INTERIOR_BLIND = "INTERIOR_BLIND", + LAPTOP = "LAPTOP", + LIGHT = "LIGHT", + MICROWAVE = "MICROWAVE", + MOBILE_PHONE = "MOBILE_PHONE", + MOTION_SENSOR = "MOTION_SENSOR", + MUSIC_SYSTEM = "MUSIC_SYSTEM", + NETWORK_HARDWARE = "NETWORK_HARDWARE", + OTHER = "OTHER", + OVEN = "OVEN", + PHONE = "PHONE", + PRINTER = "PRINTER", + REMOTE = "REMOTE", + ROUTER = "ROUTER", + SCENE_TRIGGER = "SCENE_TRIGGER", + SCREEN = "SCREEN", + SECURITY_PANEL = "SECURITY_PANEL", + SECURITY_SYSTEM = "SECURITY_SYSTEM", + SLOW_COOKER = "SLOW_COOKER", + SMARTLOCK = "SMARTLOCK", + SMARTPLUG = "SMARTPLUG", + SPEAKER = "SPEAKER", + STREAMING_DEVICE = "STREAMING_DEVICE", + SWITCH = "SWITCH", + TABLET = "TABLET", + TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR", + THERMOSTAT = "THERMOSTAT", + TV = "TV", + VACUUM_CLEANER = "VACUUM_CLEANER", + VEHICLE = "VEHICLE", + WASHER = "WASHER", + WATER_HEATER = "WATER_HEATER", + WEARABLE = "WEARABLE" +} diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts new file mode 100644 index 0000000..7b539f6 --- /dev/null +++ b/dist/esm/index.d.ts @@ -0,0 +1,10 @@ +export { default as Alex2MQTT, DEFAULT_HOST } from "./Alex2Node.js"; +export type { Alex2MQTTOptions } from "./Alex2Node.js"; +export { default as Device } from "./Device.js"; +export { AlexaInterface, AlexaInterfaceType } from "./AlexaInterface.js"; +export type { SupportedMode } from "./AlexaInterface.js"; +export { ActionMapping, AlexaActions } from "./ActionMapping.js"; +export { DisplayCategory } from "./DisplayCategory.js"; +export { AlexaStatusMessage, PowerController, EndpointHealth, TemperatureSensorScale, ThermostatMode } from "./AlexaStatusMessage.js"; +export type { ChangeCause } from "./AlexaStatusMessage.js"; +export { AlexaErrorType, AlexaErrorResponse } from "./AlexaErrorResponse.js"; diff --git a/package.json b/package.json index a827048..76d9fee 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,14 @@ "types": "./dist/types/index.d.ts", "exports": { ".": { - "types": "./dist/types/index.d.ts", - "import": "./dist/esm/index.js", - "require": "./dist/cjs/index.js" + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } }, "./package.json": "./package.json" }, diff --git a/scripts/build.mjs b/scripts/build.mjs index 9eaef3b..9c3e239 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -2,9 +2,11 @@ // // node scripts/build.mjs [--out ] defaults to dist/ in the package // -// Two compiler runs over src/: CommonJS with the declarations (cjs/, types/), then ES modules (esm/). Both write to a -// staging directory that replaces the three directories only after both runs passed, so a failed build leaves the -// tracked dist/ as it was. Files of sources that no longer exist go with the old directories. +// Two compiler runs over src/: CommonJS with its declarations (cjs/, types/), then ES modules with theirs (esm/). +// TypeScript reads a .d.ts as CommonJS or as an ES module by the package.json above it, so each build needs its own +// set: the one in types/ would tell an ES module consumer that a default import works. Both runs write to a staging +// directory that replaces the three directories only after both passed, so a failed build leaves the tracked dist/ +// as it was. Files of sources that no longer exist go with the old directories. import { spawnSync } from "node:child_process"; import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; diff --git a/test/fixtures/types.mts b/test/fixtures/types.mts index 0b7720c..bfb1d20 100644 --- a/test/fixtures/types.mts +++ b/test/fixtures/types.mts @@ -1,7 +1,10 @@ -// Compiled, never run: the same declarations as seen from an ES module. Without a "types" condition in "exports" the -// import resolves to dist/esm/index.js, which has no declarations next to it, and this file fails with TS7016. +// Compiled, never run: an ES module TypeScript consumer. The "import" condition in "exports" gives it the +// declarations next to the ES module build (dist/esm/*.d.ts), which has named exports only. Read through the +// CommonJS declarations in dist/types, the default import below compiles and then fails when Node loads it. import { Alex2MQTT, DEFAULT_HOST, EndpointHealth } from "alex2node"; import type { Device } from "alex2node"; +// @ts-expect-error the ES module build has no default export +import alex2node from "alex2node"; const bridge = new Alex2MQTT("user", "password", "root", false, { host: DEFAULT_HOST }); const devices: Device[] = bridge.getDevices(); diff --git a/test/packaging.test.js b/test/packaging.test.js index 30880c3..a87ff0b 100644 --- a/test/packaging.test.js +++ b/test/packaging.test.js @@ -37,11 +37,23 @@ test("import(\"alex2node\") loads the ES module build, with the export names of test("\"main\", \"module\" and \"types\" name the same files as \"exports\", for resolvers that do not read it", () => { const entry = pkg.exports["."]; - assert.deepEqual([pkg.main, pkg.module, pkg.types], [entry.require, entry.import, entry.types]); + assert.deepEqual([pkg.main, pkg.module, pkg.types], [entry.require.default, entry.import.default, entry.require.types]); for (const file of [pkg.main, pkg.module, pkg.types]) assert.ok(fs.existsSync(path.join(ROOT, file)), file); assert.equal(require(path.join(ROOT, pkg.main)), require("alex2node")); }); +test("each build has its own declarations, and \"exports\" names them before the code", () => { + for (const [condition, dir] of [["require", "types"], ["import", "esm"]]) { + const target = pkg.exports["."][condition]; + // TypeScript takes the first condition that matches, so "types" after "default" is never read + assert.deepEqual(Object.keys(target), ["types", "default"], condition); + assert.equal(target.types, `./dist/${dir}/index.d.ts`, condition); + const declared = files(path.join(DIST, dir)).filter((f) => f.endsWith(".d.ts")).map((f) => f.replace(/\.d\.ts$/, ".js")); + const built = files(path.join(ROOT, path.dirname(target.default))).filter((f) => f.endsWith(".js")); + assert.deepEqual(declared, built, `a declaration for every file of the ${condition} build`); + } +}); + test("the builds import Node built-ins and the declared dependencies, nothing else", () => { // "mqtt/lib/x" and "@scope/name/x" belong to the packages "mqtt" and "@scope/name" const packageOf = (specifier) => specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/"); diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 30a9249..1252904 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -3,6 +3,7 @@ "compilerOptions": { "module": "ES2020", "noEmit": false, - "outDir": "./dist/esm" + "outDir": "./dist/esm", + "declaration": true } }