@smarthome.exchange/integrations

🔌 TypeScript-native device integrations for smarthome.exchange.

This package owns discovery, configuration flows, vendor clients, mappers, events, normalized service calls, and generated integration descriptors. It keeps device-specific code out of the hub so a runtime can add, remove, publish, or mature integrations without rewriting the canonical home state model.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

Releases target both npmjs.org and the Verdaccio registry at verdaccio.lossless.digital; installation uses the unqualified package name.

pnpm add @smarthome.exchange/integrations

Use The Default Registry

import { DiscoveryEngine, createDefaultIntegrationRegistry } from '@smarthome.exchange/integrations';

const registry = createDefaultIntegrationRegistry();
const engine = new DiscoveryEngine(registry);

console.log(registry.get('hue')?.displayName);

const candidates = await engine.runActiveDiscovery({});
console.log(candidates);

Public Surface

Export group What it gives you
Core classes BaseIntegration, DescriptorOnlyIntegration, DiscoveryDescriptor, DiscoveryEngine, IntegrationRegistry, IntegrationRuntimeManager, ConsoleLogger, JsonFileConfigStore.
Core errors IntegrationError, DiscoveryError, AuthenticationError, DeviceCommunicationError.
Core types Discovery, config-flow, runtime, service-call, entity, logging, and integration status contracts.
Protocol namespaces mdns, ssdp, http, mqtt, bluetooth, and usb descriptor helpers.
Registry helpers integrations and createDefaultIntegrationRegistry(), registering handwritten integrations before filling gaps with generated descriptors.
Generated metadata generatedHomeAssistantPortIntegrations, generatedHomeAssistantPortCount, and handwrittenHomeAssistantPortDomains.
Handwritten modules All handwritten integration folders are re-exported from ts/integrations/index.ts.

Integration Lifecycle

const integration = registry.get('hue');

if (!integration) {
  throw new Error('Hue integration not found');
}

const matches = await engine.validateCandidate({
  id: 'hue:bridge:office',
  source: 'manual',
  integrationDomain: 'hue',
  host: '192.168.1.20',
  name: 'Office Hue Bridge',
  metadata: {
    baseUrl: 'https://192.168.1.20',
  },
});

console.log(matches);

The hub uses the same primitives through discoverIntegrationCandidates() and setupIntegration(...), then reconciles each runtime's complete normalized device snapshot into the canonical registry. Devices absent from a later snapshot are removed. IntegrationRuntimeManager.createRuntime() and setupIntegration() retry retained cleanup for the requested domain before invoking integration setup and fail closed with an AggregateError while cleanup still fails. Replacement setup destroys the previous runtime before constructing its successor, and overlapping same-domain setup calls are rejected. Failed replacement and mismatched-runtime cleanup remains retained under the requested domain; retryPendingCleanup(domain) is the explicit no-timeout retry entry point, while hasPendingCleanup(domain) exposes whether that gate remains closed.

Runtime setup receives an optional abort signal. Device snapshots, subscriptions, and feature writes can also accept abort signals, allowing the hub to bound setup, synchronization, and writes. A control runtime may implement writeFeature(...), callService(...), or both; authoritative readable state still comes from a later devices() snapshot rather than an optimistic write. Every runtime implements destroy() for explicit cleanup.

Home Assistant Runtime

The handwritten homeassistant integration connects to /api/websocket with a long-lived access token held only in runtime configuration. Setup does not become ready until Home Assistant has delivered the first complete entity snapshot. The official connection owns socket reconnection and subscription restoration; disconnects mark normalized devices offline immediately. Each reconnect remains unready until an authoritative state refresh and, when enabled, a fresh entity-registry provenance check both succeed. Failed reconnect refreshes use adapter-owned exponential backoff from 1 through 30 seconds. If the socket reconnects during strict startup, validation restarts against the current connection generation before setup may succeed. While an authoritative snapshot or registry refresh is pending, only selected state events are retained, coalesced by entity ID, and replayed onto the snapshot.

Optional entityIds and entityDomains are allowlists with intersection semantics when both are present. Set requireEntityIds: true to fail setup when any configured entity is absent. Set requireEntityRegistry: true together with requireEntityIds to require Home Assistant's config/entity_registry/list_for_display response and record each entity's integration platform in metadata.homeAssistantPlatform. The runtime exposes read-only state for all selected entities, controlled power and percentage brightness for supported lights, power for switches and input booleans, energy telemetry for power and energy sensors, and supported write-only vacuum commands. It does not expose unrestricted Home Assistant service calls.

import { HomeAssistantHomeassistantIntegration } from "@smarthome.exchange/integrations";

const runtime = await new HomeAssistantHomeassistantIntegration().setup({
  baseUrl: "http://homeassistant.local:8123",
  accessToken: homeAssistantToken,
  entityDomains: ["light", "sensor", "switch", "vacuum"],
  requireEntityIds: false,
  requireEntityRegistry: false,
  setupRetry: 3,
  initialSnapshotTimeoutMs: 10_000,
  serviceCallTimeoutMs: 15_000,
}, {});

try {
  console.log(await runtime.devices());
} finally {
  await runtime.destroy();
}

baseUrl and accessToken are required. entityIds and entityDomains are optional intersecting allowlists. Required-entity and registry checks need a non-empty entityIds allowlist; registry enforcement also requires required-entity enforcement, and every required entity's domain must be allowed by entityDomains when that filter is present. setupRetry must be a finite non-negative integer and controls the adapter-owned, abortable retry loop; each official websocket connection/socket call receives setupRetry: 0 so it cannot start an unbounded nested retry. initialSnapshotTimeoutMs is a timer-safe integer from 1 through 2,147,483,647 and provides one shared deadline for socket creation and authentication, initial subscription acknowledgement, the first complete entity snapshot, and optional registry verification. serviceCallTimeoutMs has the same range, defaults to 15 seconds, and bounds every write even when the caller does not supply an abort signal. Aborted or timed-out writes cancel the pending command by forcing the owned connection through its normal reconnect lifecycle. Required-entity loss blocks health and writes; recreation remains unready until registry provenance is revalidated. Among Home Assistant entity state values, unknown remains present and online with an unknown value while unavailable marks an entity offline; socket disconnection also marks normalized devices offline immediately.

The runtime requires native WebSocket support from Node.js 22.4 or newer. Production packaging must verify that runtime version. Hue, ZHA/SkyConnect, Roborock, Huawei SUN2000, and FHEM/MQTT entities remain subject to live-device acceptance testing; SUN2000 settings are not writable through this generic allowlist, and the adapter itself does not claim that those upstream integrations are configured.

Home Assistant Port Skeletons

The package includes generated native TypeScript port skeletons for upstream Home Assistant component domains under ts/integrations/<domain>. These are not Python wrappers and not a compatibility namespace. They are TypeScript classes that start as descriptor-only integrations and get replaced by handwritten clients, mappers, discovery, config-flow, and runtime code as each port matures.

Current generated metadata is exposed through generatedHomeAssistantPortCount and handwrittenHomeAssistantPortDomains; homeassistant is now one of the handwritten runtime domains. IntegrationRuntimeManager.createRuntime() treats status: 'descriptor-only' as a fail-closed invariant: it throws an IntegrationError with code DESCRIPTOR_ONLY_INTEGRATION before invoking the integration's setup(). DescriptorOnlyIntegration and Wolf Smartset also reject direct setup() calls with the same code until a real TypeScript runtime exists.

Handwritten Integrations

The default integrations array registers handwritten integrations tracked by handwrittenHomeAssistantPortDomains plus custom integrations such as Wolf Smartset. These handwritten folders are exported as named modules and registered before generated descriptors, so handwritten code wins whenever a generated Home Assistant descriptor has the same domain.

Examples include Home Assistant, Hue, AdGuard, AirGradient, Amcrest, Android TV, APC UPSD, ASUSWRT, Axis, BleBox, Bosch SHC, Broadlink, deCONZ, Denon AVR, DSMR, ESPHome, Fritz, HomeKit Controller, Homematic, KNX, Kodi, Matter, MQTT, Nanoleaf, ONVIF, Pi-hole, Plex, Roku, Shelly, Sonos, Synology DSM, TP-Link, Tradfri, UniFi, Wolf Smartset, Xiaomi Miio, Yeelight, ZHA, and Z-Wave JS.

The generator updates the barrel export file during pnpm generate:ha, so the named export surface stays aligned with preserved handwritten folders instead of drifting back to a short manual list.

CLI

pnpm cli list
pnpm cli inspect hue
pnpm cli discover
pnpm cli setup hue

When installed as a package, the binary is shx-integrations:

shx-integrations list
shx-integrations inspect hue
shx-integrations discover
shx-integrations setup hue

Regenerating Home Assistant Port Skeletons

pnpm generate:ha

Set HA_CORE_COMPONENTS_DIR to the homeassistant/components directory in a local home-assistant/core checkout before running the generator. It preserves handwritten integration folders and regenerates only folders with the .generated-by-smarthome-exchange marker.

Boundaries

This package does not own canonical device registry state, approvals, audit receipts, automations, dashboards, or persistent home state. Those stay in @smarthome.exchange/hub. Integrations normalize vendor reality into shared interface contracts and expose runtime/service primitives for the hub to consume.

Scripts

pnpm cli list
pnpm generate:ha
pnpm test
pnpm build
pnpm buildDocs

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the LICENSE file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
No description provided
Readme
4.5 MiB
Languages
TypeScript 100%