@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);
Pass abortSignal to runActiveDiscovery() to cancel discovery. Cancellation rejects with the caller's original reason even when an admitted probe reports its own error after abort, admits no further probes, and does not return a partial candidate list. An uncancelled probe failure preserves the probe's original error. Each admitted probe remains responsible for closing its owned resources.
Public Surface
| Export group | What it gives you |
|---|---|
| Core classes | BaseIntegration, DescriptorOnlyIntegration, DiscoveryDescriptor, DiscoveryEngine, IntegrationRegistry, IntegrationRuntimeManager, ConsoleLogger, NoSqlConfigStore. |
| 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. |
Durable Integration Config
NoSqlConfigStore implements IConfigStore through the public document-model APIs of @lossless.org/client/nosqldb. It replaces JsonFileConfigStore; application config is no longer persisted on the filesystem. Supply a connected, caller-owned SmartdataDb, an authenticated home ID, an integration domain and an external key provider:
This is a breaking API change: remove the JsonFileConfigStore import and replace new JsonFileConfigStore(filePath) with the options-based constructor below. Await initialize() before passing the store to integration contexts. Existing IConfigStore calls to get(), set(), delete() and list() remain valid; each also accepts optional transaction options. Existing JSON files are not read automatically; import them explicitly with the migration described below before switching the application to the new store. The database and encryption-key provider are required to keep config durable, scoped and encrypted.
import { NoSqlConfigStore } from '@smarthome.exchange/integrations';
const store = new NoSqlConfigStore({
db,
homeId: authenticatedHome.id,
integrationDomain: 'hue',
keyProvider: {
currentKey: (signal) => secretProvider.currentEncryptionKey(signal),
keyForId: (keyId, signal) => secretProvider.encryptionKeyById(keyId, signal),
},
});
await store.initialize();
await store.set('bridge', bridgeConfig);
const restoredConfig = await store.get('bridge');
db, authenticatedHome, bridgeConfig and secretProvider are application-owned collaborators. The store neither creates nor closes the database. Initialize it before opening a transaction so its indexes are ready. Choose its immutable home/domain scope from authorized server context. IConfigStore injection remains available in discovery, config-flow and setup contexts.
getScope() returns a new frozen IConfigStoreScope containing the immutable homeId and integrationDomain captured at construction. The snapshot remains available after close and contains no database or encryption credentials. IConfigStore exposes this capability optionally; integrations requiring scoped storage must reject a missing or mismatched scope before accessing config. A scope declaration does not grant home access: compare it against verified server context.
initialize({ signal }) prepares the declared collection and indexes through the client's public prepareCollection() API. The configured timeoutMs bounds the entire preparation, including waiting for another preparation and cleanup; an optional caller signal cancels that caller's attempt. Failed or cancelled preparation can be retried on the same open store. Await close() during shutdown: it stops admission, cancels this store's preparation and key lookups, refuses queued operations and joins admitted work, including the actual encryption-provider callbacks after a caller deadline expires. It publishes one stable promise before abort listeners run. A provider that never settles keeps cleanup pending. An unrelated provider rejection after cancellation produces a sanitized cleanup failure after all siblings settle; repeated close() calls retain that original rejection. Explicit terminal retryCleanup() acknowledges only settled retained provider failures. It does not call the provider or database again, replay a config operation or reopen the store. Resource owners must use this method for explicit cleanup retry. In-flight database operations and caller-owned transactions retain their client deadlines and ownership; close does not close the database, session or key provider. A closed store cannot be reopened.
Every config value, including credentials, is encrypted with AES-256-GCM and a fresh 12-byte nonce. currentKey(signal) returns { id, key }, where key contains exactly 32 bytes; keyForId(id, signal) resolves an older key or returns undefined. Keep keys in an external secret provider, preserve old keys while records still reference them, and honor cancellation. The store binds the ciphertext to its schema version, key ID, home ID, integration domain and config key. A missing key, corrupt envelope or failed authentication rejects the read. Normal writes use the current key, so rewriting a value rotates its encryption without changing its scope.
The database stores one document per config key with unique scope indexes. Atomic writes preserve unrelated keys across independent instances; get() does not cache values. Operations on one store execute in invocation order, including read-after-write ordering. Admission defaults to 1,000 active, queued or retained operations and rejects at capacity. An operation uses one slot while active; after its caller deadline expires, pending actual provider work retains that slot until fulfillment. A late unrelated provider failure retains it until explicit terminal cleanup acknowledgment. Ordinary settled failures release admission. timeoutMs defaults to 5,000 and bounds each key-provider lookup and each database call outside a transaction. Values must be finite JSON data, with dense arrays and plain objects, at most 32 levels deep and 64 KiB by default. Optional object fields whose value is undefined are omitted; undefined array elements and top-level values reject. maxValueBytes can raise that limit to 1 MiB; large payloads belong in object storage. create() inserts without replacing an existing key.
All config operations accept IConfigStoreOperationOptions with a public SmartdataSession from the same database. Call initialize() before starting a transaction, then sequentially await get(key, { session }), set(key, value, { session }), create(key, value, { session }), delete(key, { session }) and list(prefix, { session }) inside session.withTransaction(callback, { timeoutMS, signal }). Reads and lists see that transaction's writes, and config writes can commit or roll back with another model's ownership-lease update. The caller owns commit, rollback and session close. Session-carrying operations omit their database timeout because the caller's transaction supplies it; always provide a transaction deadline. Key lookup keeps its separate store deadline, so cancellation during a lookup waits for that lookup to settle or time out before the transaction rejects. No database operation is detached on timeout; the client settles its operation and rollback cleanup before rejecting. Do not retry an unknown commit outcome: inspect SmartdataSession.getCommitOutcome(error) and reconcile durable state first.
Existing JSON files can be imported explicitly through importLegacyJsonConfig() from @smarthome.exchange/integrations/migration. Pass the same store options plus sourcePath. The versioned module under ts_migration/ reads at most 1 MiB, requires an object root, and uses one transaction to insert all values and record completion. Any existing config key refuses the entire import. Re-running the same completed source returns { applied: false, importedKeys } without replacing subsequent edits; a changed source rejects. The source file is never written, renamed or removed, and no file import runs implicitly during normal store initialization. Migration requires a database supporting transactions.
Integration Lifecycle
const integration = registry.get('hue');
if (!integration) {
throw new Error('Hue integration not found');
}
const matches = await engine.validateCandidate({
id: '001788fffe123456',
source: 'manual',
integrationDomain: 'hue',
host: '192.168.1.20',
name: 'Office Hue Bridge',
});
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, a fresh service catalogue, and a fresh entity-registry provenance check all 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 an entry for every selected entity in Home Assistant's config/entity_registry/list_for_display response. Every setup records available entity integration platforms 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 integration = new HomeAssistantHomeassistantIntegration();
const runtime = await integration.setup({
homeId: "house-a",
baseUrl: "http://homeassistant.local:8123",
accessToken: homeAssistantToken,
entityDomains: ["light", "sensor", "switch", "vacuum"],
requireEntityIds: false,
requireEntityRegistry: false,
setupRetry: 3,
initialSnapshotTimeoutMs: 10_000,
serviceCallTimeoutMs: 15_000,
}, { homeId: "house-a" });
try {
console.log(await runtime.devices());
} finally {
await integration.destroy();
}
homeId, baseUrl, accessToken and independently selected context.homeId are required. The first admitted home stays fixed after failed readiness; optional real NoSql stores must match the exact home and homeassistant domain. Native ownership shares a capacity of 16 across setup, runtimes and retained cleanup. Integration shutdown drains its runtimes and forbids reopening; explicit retryCleanup() retries cleanup only. Device and entity IDs include the configured home and a stable hash of the endpoint origin; writes validate that immutable scope and the current canonical device/feature before dispatch. The integration always fetches real service and entity-registry catalogues before readiness, and only currently advertised service controls are writable. Catalogue-change events and reconnects refresh all three authoritative snapshots. See the Home Assistant runtime for the configuration migration and exact boundaries. 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, live service catalogue and entity-registry provenance. 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.
Home Assistant reads, writes and asynchronous subscriber deliveries also share native owner capacity. Caller cancellation rejects promptly while the actual continuation remains owned; unsubscribe fences future delivery and joins accepted callbacks. Repeated destroy preserves its original result, including callback and cleanup failures; explicit retry performs cleanup without replay. Borrowed stores and host resources remain open for the host to close.
Terminal subscription qualification is blocked by home-assistant-js-websocket 9.7.0: an unacknowledged subscription never settles after terminal socket close. The inspected 9.7.1 artifact retains the same implementation. HA continues owning that work until an upstream fix can be consumed; see the domain qualification notes.
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.retryPendingCleanup() uses a runtime’s explicit retryCleanup() hook when available after an attempted close; repeated terminal destroy() results do not reopen or replay native work. 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, Tasmota, TP-Link, Tradfri, UniFi, Wolf Smartset, Xiaomi Miio, Yeelight, ZHA, and Z-Wave JS.
The native Tasmota runtime connects selected devices directly through the shared MqttSession. Setup and config flow require the embedding host's independently selected home; authentication explicitly selects anonymous mode or a broker-bound encrypted credential reference. Relay and single-channel brightness writes require constructor enableControls: true and fresh nonretained device state; sensor telemetry remains read-only. The shared native owner bounds operations and runtimes, joins actual credential and command work during shutdown, and retains failed cleanup for explicit retryCleanup(). The adapter is registered as control-runtime.
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
Release preflight on the shared Linux build host acquires $HOME/.cache/shx-heavy.lock separately for the complete serial pnpm test suite and pnpm build. Each lock wait is bounded to 240 seconds; exit 75 means the lock was unavailable, so retry the preflight after the other heavy check completes. Publication and registry verification run after those check processes release the lock.
Native Home and Lifetime Ownership
Native adapters using the shared ownership core require homeId in their setup and config-flow context, selected independently by the embedding host. An optional constructor home is an additional fixed constraint; it never supplies a missing context home. Configuration, editable fields, discovery metadata and storage scope cannot choose the host home. Generic registry factories remain inert. The first validated context fixes the instance home before asynchronous work, including when setup later fails. A different home needs a new instance.
This is an embedding-host contract. It grants no authentication or authorization by itself. A Hub must select an explicit operator home and enforce its authenticated request boundary before passing context. Display defaults and request payloads are not home authority.
An optional NoSqlConfigStore must be an actual instance with the exact selected home and integration domain. It remains borrowed with its database and key provider. Protocol-only paths may omit it; encrypted credential-reference or restoration paths still require their documented persistence capability. No store scope or fallback storage supplies a home.
NativeHomeAdmission, NativeIntegrationOwner and its child flow scopes provide reusable admission and cleanup for native adapters. The default capacity16 counts pending work, live runtimes, retained failed cleanup and additional owned resources. Register cleanup before foreign callbacks, capture the admitted context, and promote only prepared runtimes. Close is terminal and joins actual admitted work and sibling cleanup even if one fails. It publishes its stable promise before synchronous abort listeners run. Caller cancellation closes its retained runtime lease.
NativeIntegrationOwner.runOperation(context, configHomeId, work) admits an operation through the same root ownership and home checks without allocating a permanent config-flow scope. The callback receives the existing captured operation lease. Live runtimes, pending operations and retained failed cleanup share the configured capacity; a live runtime plus a new operation uses two slots, so capacity 1 refuses that extra operation. A domain runtime must still reject calls after its own terminal close and join its actual command and subscriber continuations. No home string or operation lease grants authentication or control permission.
Cleanup failures remain observable and retain capacity. retryCleanup() explicitly retries failed cleanup only; it never repeats setup or transport effects or reopens the owner. Completed admitted persistence writes keep their actual result during close. Borrowed persistence resources are never closed by an adapter, and cancellation makes no claim that a physical effect was rolled back.
Native ESPHome admission requires independent home context and selected observed node name/MAC, with optional model consistency. Persist only a credential reference and use the actual SDK psk option through an explicitly selected borrowed provider or encrypted store. Existing documented controls remain, with fresh readiness and matching reported-state confirmation; actual callbacks and SDK disconnect are joined within their public guarantees. See the ESPHome contract.
Native Z-Wave JS keeps SHX home admission separate from the numeric Z-Wave network ID. Proxy credentials use references and an explicitly selected borrowed provider or encrypted store. Controls default to read-only and require fresh authoritative writable values; supervised replies preserve their device-reported outcome with causality unconfirmed. See the Z-Wave JS contract for schema32, canonical target migration and terminal cleanup.
Native MQTT setup and flows require independent home context and reference-only credentials, with an explicit borrowed resolver for each selected credential reference. The caller can implement that resolver through matching encrypted storage; no automatic source fallback is supplied. Selected-topic mode is opt-in read-only exact-topic observation; discovery controls and the reusable raw MQTT client credential API remain available. MqttClient.getCurrentSnapshot() synchronously exposes only the current live connection observations for readiness, while getSnapshot() preserves historical discovery metadata. See the MQTT contract for migration, TLS, actual callback drain and acknowledgment limits.
Native Shelly Gen2/3 setup and flows require independent home context, exact selected reported identity and reference-only credentials. Output controls default to read-only and require current methods and independent readback, with unconfirmed physical causality and no setter replay. Existing push updates, Digest authentication and Gen3/two-channel mappings remain available. See the Shelly contract for public configuration, credential lifetime and fixture qualification.
Native Sonos requires independent home context and explicit constructor control permission, defaulting to read-only. Fresh selected identity, topology, current transport actions and service declarations govern coordinator playback and member rendering. Sequential play_media keeps one admission and deadline, refreshes authority between its two effects, and reports acknowledged actions and independent readback without claiming atomicity or physical causation. Cleanup joins actual command and subscriber continuations and retains failures for explicit cleanup-only retry. See the Sonos contract.
The native deCONZ integration requires an independently selected host homeId, a matching reference-only configuration (credentialReference, selected bridgeId, endpoint), and an explicit credential provider or an actual encrypted home/domain config store. Runtime controls default to denied and require exact constructor resource/field permissions. Supported lighting, groups, covering, thermostat and CLIP controls validate current reported resources, send one command and independently read state; readback never confirms causality, physical completion or group atomicity. Shared ownership bounds pending work and live subscriptions, joins actual asynchronous callbacks, retains failed cleanup and supports explicit cleanup retry without replay. Caller-injected stores, databases and key providers remain caller-owned. Pairing returns credential references, never durable inline API keys. See ts/integrations/deconz/readme.md for the migration and permission contract.
License and Legal Information
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.
Nanoleaf native local controller
Every setup and config-flow call requires a separately selected host context.homeId; constructor binding, controller config and encrypted store cannot supply it. Nanoleaf configuration requires an explicit matching homeId, selected literal private/loopback/link-local unicast IPv4 HTTP(S) origin, exact expectedSerialNumber, documented Light Panels API expectedModel, and credentialKey. Use a caller-supplied encrypted NoSqlConfigStore scoped to that home and the nanoleaf domain, or construct NanoleafIntegration({ db, keyProvider }) with public @lossless.org/client/nosqldb and an external encryption key provider. The integration closes stores it creates and retains caller-owned stores, database and key provider.
The config flow takes an existing token, validates the selected controller's real identity, and insert-only persists a versioned identity/endpoint-bound token envelope in the encrypted public store. It returns only a credential reference, never the token. The caller can provision or rotate an existing token with the public nanoleafCredential(config, token) and nanoleafCredentialKey(config) helpers and store.set(...); each admitted operation retrieves the current encrypted record. Pairing and revocation remain explicit external operations.
Vendor plaintext HTTP requires allowUnencryptedHttp: true; HTTPS preserves certificate and hostname checks and optionally accepts an explicit certificateAuthority PEM. These selected local endpoints are not cryptographic Nanoleaf identities over plaintext HTTP. Advertisements are setup hints only, and runtime identity is verified against exact serial/model before controls.
One home-scoped aggregate controller is mapped from current observations. Power is absolute boolean; brightness is exact integer percent inside its current advertised range; color temperature is exact Kelvin inside its current advertised range. A select entity and scene control exist only for current non-reserved advertised scenes. Numeric capabilities absent in current observations are not fabricated. Service targets require the exact controller ID or corresponding light/scene entity. Writable features forward caller cancellation. Every control is submitted once, and reports HTTP acknowledgement separately from fresh observed state and physicalCompletion: 'unconfirmed'. A response may acknowledge a command without changing observed state; an ambiguous transport/readback after submission is indeterminate and never replayed.
Breaking migration: legacy host/port, inline authToken/token, injected cached snapshots, optimistic updates, arbitrary effects, hue/RGB conversion, identify, per-panel/rhythm inference, increments/transitions and no-op destruction are removed. Configure the explicit identity-bound encrypted token reference and use light.turn_on, light.turn_off, light.set_brightness with { brightness }, light.set_color_temperature with { kelvin }, or select.select_option with { option }. nanoleaf.snapshot reads current observations. Destroy is terminal, publishes one promise before abort callbacks and joins admitted credential callbacks, owned HTTP/TLS requests, stores created by the adapter and setup work. The shared owner retains failed cleanup for explicit retryCleanup() without reopening or replaying effects.
Verified only with local real HTTP/TLS fixtures and disposable public-client NoSQL fixtures. No real controller, firmware behavior or physical completion was checked.
Native WiZ selected-device runtime
WiZ uses @ecobridge.xyz/devicemanager's published WizProtocol. Supply an exact homeId, private/link-local/loopback unicast IPv4 host, and unicast macAddress; optional port defaults to 38899. Hostnames, public addresses, broadcast, snapshots, manual capability declarations, arbitrary command executors and raw setPilot are rejected. Supplied mDNS/UDP/manual advertisements are unauthenticated setup hints only. This integration does not broadcast registration or provision devices.
Setup and config flow require independent explicit context.homeId; constructor binding, config, editable fields and store scope cannot supply it. An optional actual NoSqlConfigStore must match that home and the WiZ domain and remains borrowed with its database and key provider. Protocol-only setup and flow work without a store. When a store is supplied, the flow verifies current system/pilot MAC identity before saving the selected config under device:<normalized MAC>; mismatched or failed admission does not persist a selection. Runtime shutdown closes its shared owner lease without closing the borrowed store; failed owned cleanup remains observable until explicit retryCleanup().
Known single-head RGB/tunable-white/dimmable-white lights and sockets expose only the capability subset admitted by current module identification and actual pilot fields. Unknown, dual-head and fan models remain observable and read-only. Only explicit power, brightness (integer 1–100), and RGB (three integer bytes) controls are available. Temperature and scene IDs are observed read-only values; no invented scenes, toggles, fan/dual-head mappings, white-temperature writes or compound commands are supported.
Device and entity IDs include the exact encoded home ID and MAC. Services require an owned device/entity target and reject conflicting targets, accessors and unsupported fields before dispatch. light.turn_on/light.turn_off accept no data, light.set_brightness accepts exactly { brightness }, and light.set_rgb accepts exactly { rgb: { red, green, blue } }. Sockets use switch.turn_on/switch.turn_off. Canonical feature writes ignore caller-supplied writable flags and require exact home/device ownership and an observed supported feature; RGB feature values use "red,green,blue" decimal bytes.
Every control gets fresh identity/capability preflight and sends its effect once. Results retain not-sent, rejected, indeterminate or accepted and always report physicalStateConfirmed: false. A positive firmware acknowledgement means acceptance; independent readback may disagree. If accepted readback fails, the result keeps outcome: 'accepted' with the protocol error code, success: false, and no automatic replay. Observed state never receives an optimistic desired-value patch.
Subscriptions deliver current authoritative observations and poll serially only while subscribed. pollIntervalMs defaults to 30000; zero disables polling and positive values must be at least 100 milliseconds. Unsubscribe cancels owned polling and removes its abort listener. Caller lifetime cancellation and terminal/idempotent destruction drain admitted operations and UDP sockets, remove subscriptions/timers, and permanently forbid new work. Synchronously failing subscription callbacks are removed and reported through the supplied logger; initial delivery errors reject subscription setup. The protocol's lifetime exchange bound (maxExchanges, default 4096) remains explicit; exhaustion closes polling and requires a new explicitly set-up runtime.
WiZ LAN UDP is not cryptographically authenticated. Address/MAC/source-port consistency does not establish authorization, attacker-resistant replay protection or physical actuation. The hub must enforce caller identity, home grants and scopes before calling integrations. RGB/brightness can have firmware-defined mode or power effects even though only the caller's explicit supported fields are sent. No hardware was accessed; model-specific firmware and physical actuation remain unverified.
Protocol sources: pywizlight bulb implementation and pywizlight model/capability identification.
Selected Denon and Marantz receivers
DenonavrIntegration uses the published @ecobridge.xyz/devicemanager
DenonProtocol. Supported profiles are Denon AVR-1913/AVR-2113CI and Marantz
NR1605/SR5009. One runtime selects one receiver and its main zone.
const integration = new DenonavrIntegration({ homeId: 'my-home' });
const runtime = await integration.setup({
homeId: 'my-home',
host: '192.168.1.42',
descriptionUrl: selectedDescriptionUrl,
expectedUdn: selectedReceiverUdn,
model: 'AVR-1913',
maximumVolumeDb: -10,
}, { homeId: 'my-home', configStore });
try {
const entity = (await runtime.entities())[0];
const result = await runtime.callService!({
domain: 'media_player',
service: 'volume_set',
target: { entityId: entity.id },
data: { volume_db: -30.5 },
});
console.log(result.success, result.data);
} finally {
await integration.destroy();
}
Each setup/config-flow call requires independent host-selected context.homeId. Configuration, constructor binding, discovery and store scope cannot replace it. The credential-free path may omit storage. An optional actual NoSqlConfigStore must match the exact admitted home and denonavr domain; the caller retains its store, database and key provider. The public DenonavrClient(config, context) also requires the explicit host context. This credential-free
protocol does not write runtime observations or configuration, generate
credentials, access MongoDB directly or persist on disk.
Supply an explicit private or loopback IPv4 address, the selected receiver's same-IP HTTP UPnP description URL, its exact UDN and supported model, and an owner volume ceiling. No HTTP descriptor path is guessed. Discovery records are unauthenticated setup hints. Setup and every read/effect obtain fresh receiver identity and power/mute/volume/input observations. A valid vendor descriptor with the selected identity is required; descriptor spelling and availability on physical firmware remain unverified. Plaintext UPnP and TCP observations do not cryptographically authenticate the LAN peer.
Services are turn_on, turn_off, volume_mute with boolean
is_volume_muted, volume_set with absolute numeric volume_db, and
select_source with an exact observed physical source. Feature writes use
power, muted, volume_db and input. Volume must lie on the documented
half-decibel grid from -79.5 to +18 dB and may not exceed maximumVolumeDb.
The vendor minimum-volume sentinel remains null. Inputs must already have
been observed on this receiver and belong to the protocol's documented
physical subset; display-name maps and streaming shortcuts are not authority.
Every service or feature write must target the selected receiver's exact home/UDN-derived device or entity ID. These IDs survive renames and distinguish homes. Configuration rejects caller-supplied snapshots, zone/source maps and unverified receiver information. Unsupported models, extra zones, incremental volume, toggles, streaming/playback commands, arbitrary HTTP/TCP commands and HEOS/account features are outside this runtime.
Each effect is submitted once. Result data.dispatch: 'written' describes
local TCP submission; data.outcome: 'observed-target' means the fresh
receiver observation matched. success reports that match, without proving
command causality, electrical output or audible playback. A different setting
returns success: false with outcome: 'indeterminate'. Protocol failures
carry data.effectOutcome: 'not-sent' | 'indeterminate'. Effects are never
automatically retried or replayed. Power observations produce only on/off
media states, without inventing playing/paused state.
An adapter FIFO rechecks exact home scope when admitted work actually runs.
The configured deadline includes both adapter queue time and all protocol
preflight/write/readback work; active plus waiting operations are bounded.
Each operation drains its fresh HTTP/TCP resources. Destruction cancels and
joins startup and admitted work, detaches owned listeners and is terminal.
subscribe() reports newly completed adapter observations; it creates no
background polling, replay or automatic reconnect loop. Subscriber exceptions
detach that listener and are surfaced by its unsubscribe or runtime teardown.
The test suite uses real bounded local HTTP/TCP fixtures. No physical receiver or account was contacted. Receivers configured with network access disabled in standby remain unreachable; wake/retry behavior is not inferred.
Protocol references: Denon AVR-2113CI/AVR-1913 protocol, Marantz 2014 protocol workbook, UPnP Device Architecture.
Onkyo now uses the published selected main-zone OnkyoProtocol from
@ecobridge.xyz/devicemanager@4.5.0 and canonical tcp from interfaces 2.1.
Setup and config flow require independent explicit context.homeId; optional constructor home checks consistency and never supplies missing context. The public OnkyoClient(config, context, signal?) requires this context too. Optional actual matching NoSqlConfigStore remains borrowed; protocol-only calls need none. Configuration requires matching home, private selected IPv4 host, exact MAC/model,
cleartext LAN acknowledgement and an integer raw-volume ceiling. Native power,
mute, advertised input selectors and integer raw volume are admitted from fresh
receiver identity/capability observations. Events report explicit inspections
and matching control reports with acknowledgement/causality unconfirmed. See
the native Onkyo contract for exact services,
feature ids, scopes, shutdown behavior and fixture-only verification limits.
Selected native Daikin BRP069
DaikinIntegration uses the published devicemanager DaikinProtocol for one selected controller. Native configuration requires homeId, explicit apiVariant: 'brp069', private literal host, exact twelve-hex-digit expectedMac, and allowPlaintext: true. Configuration flow and setup independently verify current basic/model/control observations. Only exact owned targets with explicit boolean power or integer Celsius targets in the currently observed standard cool/heat range can dispatch effects. Required current control fields are preserved by the protocol, and accepted setters require matching independent readback for success. Mismatches and post-dispatch failures remain indeterminate; there are no effect retries, optimistic state or fabricated ambient sensors/HVAC action.
Discovery is passive. Setup, config flow and public DaikinClient(config, context) require independent explicit host-selected context.homeId. Optional constructor home only checks consistency. An optional actual NoSqlConfigStore must match that home and daikin domain and remains borrowed; protocol-only operation requires no store. Runtime and integration destruction cancel and drain owned protocol work and subscriptions. Integration and configuration-flow destruction await all owned cleanup settlements before reporting aggregated original failures; repeated destruction returns the same terminal result without retries. The legacy injected-client/executor/static setup path is replaced by explicit verified native setup; offline mapper/parser/translation utilities remain read only. See the Daikin runtime for migration and boundaries. Real HTTP/NoSQL fixtures pass; physical hardware remains unverified.
Selected native Broadlink RM IR
Broadlink setup selects one authorized home, exact private IPv4 endpoint, six-byte
unicast MAC, and RM mini 3 type 0x2737 or RM4 mini type 0x51da. Other Broadlink
families and model variants are unsupported by this runtime. Provision the device
on the trusted LAN separately. Discovery candidates do not authenticate a bridge;
setup verifies the selected MAC/type, establishes one encrypted session, and reads
the observed firmware through the published @ecobridge.xyz/devicemanager API.
Every setup/config-flow call and public BroadlinkClient(config, context) requires independent explicit host-selected context.homeId. Optional constructor binding, config and storage scope cannot supply it. Explicit owner actions allow protocol-only operation without storage. Restoring omitted actions requires an initialized actual NoSqlConfigStore matching the admitted home and broadlink domain. The host retains user/action authorization; structural or unscoped stores are rejected. When supplied, the borrowed store persists selection and owner IR actions
through the public client document models with its existing AES-256-GCM encryption.
It never stores Broadlink session keys. There is no filesystem or direct MongoDB
access, and no plaintext persistence fallback.
const store = new NoSqlConfigStore({
db, homeId: authorizedHome.id, integrationDomain: 'broadlink', keyProvider,
});
await store.initialize();
const integration = new BroadlinkIntegration();
const runtime = await integration.setup({
homeId: authorizedHome.id,
host: '192.168.1.55',
macAddress: 'a0:43:b0:54:44:6e',
deviceType: 0x2737,
actions: [{ id: 'owner_tv_code', name: 'Owner TV IR code', packetBase64: approvedNativeIrBase64 }],
}, { homeId: authorizedHome.id, configStore: store });
try {
const [device] = await runtime.devices();
const action = device.features.find((feature) => feature.id === 'ir.owner_tv_code');
if (!action) throw new Error('Configured action missing');
const result = await runtime.writeFeature!({ device, feature: action, value: true });
// result.success confirms bridge acceptance only.
// result.data.physicalEffectConfirmed is always false.
} finally {
await integration.destroy();
await store.close();
}
The application supplies db, authorizedHome, keyProvider, and its already
approved native IR payload. actions must contain 1–32 unique lowercase IDs,
display names and canonical base64 packets. The public upstream validator rejects
non-IR packets, repeats, malformed lengths/pulse pairs and packets outside its
6–1024 byte limit before encrypted persistence. This runtime generates no waveform
and assigns no appliance meaning to arbitrary bytes. Omit actions only to restore
an existing encrypted record matching the exact home/MAC/type/host/port/local
interface selection. Stored actions are revalidated before startup.
The selected bridge maps to a canonical device ID containing home and MAC. Its
readable state is observed connectivity and numeric firmware. Owner actions are
write-only ir.<actionId> features and button entities with state: null; sending
one never changes appliance power, volume, temperature, playback or other state.
Feature writes require the canonical bridge/feature and trigger value true.
Services support button.press targeting a known button entity, or
broadlink.send_ir targeting the known bridge with only {actionId}. Cross-home,
unknown, contradictory and missing targets fail before any UDP exchange. Raw
service payloads, timings, repeats, arbitrary command names, learning, RF,
switch/appliance services and injected executors are rejected.
port defaults to 80, timeoutMs to 5000 (maximum 60000), and
maxPendingOperations to 32 (maximum 128). An optional private localAddress
selects an interface; loopback addresses are admitted for local fixtures. The
public foundation owns bounded FIFO/deadlines, identity/session/counter/checksum
and exact source validation, cancellation and terminal socket/key cleanup. The
consumer adds no retry, reconnection, automatic reauthentication or effect replay.
Setup and snapshot/feature operations honor caller cancellation; destroy stops
admission and joins admitted work while leaving the caller's store/database open.
Protocol-close failures still drain admitted work and clear owned IR buffers.
Integration destroy waits for every runtime and aggregates cleanup failures;
failed runtime cleanup remains owned for explicit retryCleanup(). Repeated destroy preserves its original terminal promise; cleanup retry never reopens the session or replays an action. Setup reports both its original failure and any
cleanup failure.
Recreate the runtime explicitly after terminal session failure.
Every result reports physicalEffectConfirmed: false. Valid acknowledgements
report outcome: 'accepted'; errors preserve not-sent, indeterminate, rejected
or accepted from the protocol. An unknown/corrupt/lost reply after UDP handoff
remains indeterminate and is never retried. Protocol default keys, fixed IVs and
checksums do not authenticate the peer cryptographically. MAC/IP matching protects
against accidental misbinding on a trusted isolated LAN and cannot establish user
authorization or resist an active LAN attacker.
This is a breaking migration from snapshot/injected-executor setup. Replace the
old IBroadlinkConfig with explicit selected configuration and scoped storage;
remove snapshot, codes, switches, commandExecutor and transport. Pure
offline snapshot/command mappers and packet encoders remain available explicitly,
with old snapshot input typed as IBroadlinkOfflineConfig; they do not start a
runtime, validate native admission or prove physical effects.
Real encrypted loopback UDP and encrypted in-memory NoSQL tests verify this strict protocol envelope. Real hardware, firmware/model variants and physical appliance effects have not been verified. The vendor's RM4 mini product description and IR-code API overview describe IR transmission and external code storage; the lower-level LAN contract and its security limitations are documented in devicemanager's readme.
Yamaha selected RX-V485 main zone
YamahaIntegration admits the main zone of an explicitly selected RX-V485 through the published @ecobridge.xyz/devicemanager Yamaha protocol. Setup and config flow require independent explicit host-selected context.homeId; optional constructor home only checks consistency. Configuration, discovery, form and store scope cannot replace it. Configuration requires a matching homeId, a literal private/link-local/loopback IPv4 HTTP origin, the exact 12-character Yamaha device_id, the selected model, explicit allowUnencryptedHttp: true and an integer maxVolume owner ceiling in the receiver's raw vendor units. It never infers percent/dB volume or accepts arbitrary snapshots, executors or raw protocol commands as a live runtime.
import { YamahaIntegration, IntegrationRuntimeManager } from '@smarthome.exchange/integrations';
const integration = new YamahaIntegration();
const manager = new IntegrationRuntimeManager();
try {
const runtime = await manager.setupIntegration(integration, {
homeId: authenticatedHome.id,
origin: 'http://192.168.1.50',
expectedDeviceId: '00A0DE123456', // Replace with the selected receiver's device_id.
model: 'RX-V485',
allowUnencryptedHttp: true,
maxVolume: 60, // Owner-chosen raw ceiling within this receiver's current advertised range.
}, { homeId: authenticatedHome.id, abortSignal: homeLifetime.signal, configStore: optionalYamahaHomeStore });
const device = (await runtime.devices())[0];
await runtime.callService?.({ domain: 'media_player', service: 'volume_set',
target: { deviceId: device.id }, data: { volume_raw: 40 } });
} finally {
await manager.destroyAll();
await integration.destroy();
}
The host owns authenticatedHome, its lifetime signal and the optional config store. The exported YamahaClient(config, context) requires the same independent native context, including homeId; its second argument is no longer a config store. A supplied store must be an actual NoSqlConfigStore matching that exact home ID and integrationDomain: 'yamaha'; routine setup/reads/controls neither read nor write it, and shutdown never closes it, its database or its key provider. YamahaConfigFlow verifies fresh live identity/features/status before explicitly persisting selected config to an injected scoped store. Without a store, it returns the verified immutable config. Flow destruction cancels owned requests, refuses stale forms and joins admitted caller-store writes; a write already admitted may complete while the form closes and preserves its actual done result. Failures from writes admitted before closure remain visible in the closure's aggregate error.
Device features are power, muted, volume_raw (unit raw) and input. Media-player services are turn_on, turn_off, volume_set with { volume_raw: integer }, volume_mute with { is_volume_muted: boolean }, and select_source with { source: currentAdvertisedInput }. Every request targets the exact home/receiver-derived device or entity ID and refuses extra parameters. Input selection uses autoplay_disabled. The protocol refreshes identity/features/status before every effect and attempts fresh readback after its single dispatch. Vendor volume grid, receiver max-volume and owner ceiling are enforced; no toggles, relative volume, arbitrary commands, playback/linking or other zones/models are exposed.
Results distinguish acknowledged dispatch from observed target. success is true only when the fresh observation matches; an unchanged setting yields an indeterminate receipt with the actual observed state. A missing/malformed acknowledgement, cancellation, identity/scope drift or failed readback after dispatch yields effectOutcome: 'indeterminate'. Nothing retries or replays. Inspect actual state deliberately before another call. Receiver observations do not prove acoustic/electrical effects or authenticated identity; independent controllers may change state between observations, so this is not a physical volume guarantee.
Subscriptions emit copied completed observations from explicit adapter calls; they do not start hidden polling or Yamaha UDP subscriptions. Each subscriber can own at most one asynchronous callback; a second observation before it settles terminates that subscription with an inspectable cleanup error. Returned callback promises must settle, and unsubscribe/runtime shutdown joins them; callback failures surface through cleanup. Owner abort closes a runtime terminally, joins owned HTTP drain and refuses later operations. Queue/response/subscriber/runtime capacity and operation deadlines are bounded. YamahaOfflineMapper retains pure raw-data mapping for imports/analysis, with unavailable/unwritable output that cannot be supplied to native setup.
Evidence: Yamaha RX-V485 product information and Yamaha-authored Extended Control Basic Rev2 specification, using documented v1 fields. API >=1.17 is required for the stable device_id and every required identity/capability/status field must be observed; the Rev2 preface describes API >=2.00 firmware, so this does not claim all Rev2 features on earlier firmware. LAN HTTP is plaintext and identity matching does not protect against malicious LAN peers. Verification uses synthetic loopback fixtures; physical receiver/firmware behavior remains unverified.
The shared owner bounds pending setup and flow, live runtimes and retained cleanup resources together at16. Clients and actual observer promises are owned before foreign callbacks. Failed cleanup stays observable and consumes capacity until explicit retryCleanup() succeeds without replaying setup or transport work. Closing retains its original terminal promise.
Selected Pioneer SC-95 receivers
PioneerIntegration admits the SC-95/CUXJ main zone through the published @ecobridge.xyz/devicemanager PioneerProtocol. Select a private or loopback IPv4, an existing UPnP description URL at that same address, exact receiver UUID/model and raw owner volume ceiling. Explicit allowUnencryptedLan: true acknowledges plaintext HTTP/TCP. Discovery and UPnP identity establish consistency with a selected LAN endpoint; they do not cryptographically authenticate the peer.
import { PioneerIntegration } from '@smarthome.exchange/integrations';
declare const hostSelectedHomeId: string; // Selected independently by the trusted embedding host.
declare const selectedDescriptionUrl: string;
declare const selectedReceiverUdn: string;
const integration = new PioneerIntegration();
try {
const runtime = await integration.setup({
homeId: hostSelectedHomeId, host: '192.168.1.80', descriptionUrl: selectedDescriptionUrl,
expectedUdn: selectedReceiverUdn, model: 'SC-95', allowUnencryptedLan: true,
maximumVolumeRaw: 121,
}, { homeId: hostSelectedHomeId });
const device = (await runtime.devices())[0];
const muted = device.features.find((feature) => feature.id === 'muted');
if (!muted) throw new Error('Selected receiver has no mute feature.');
const receipt = await runtime.writeFeature!({ device, feature: muted, value: true });
console.log(receipt.data);
} finally {
await integration.destroy();
}
Setup, direct new PioneerClient(config, context) and config-flow start require an explicit context.homeId selected by the embedding host. Config, discovery, editable fields and store scope never select that home. new PioneerIntegration({ homeId }) and standalone new PioneerConfigFlow({ homeId }) optionally bind an immutable constructor home as a further consistency constraint; context home remains required. Without constructor binding, the first valid host admission fixes the home synchronously before receiver I/O, including a later failed identity probe. Integration and its flow share this admission. This is a trusted host contract, not a string-as-authentication claim.
Pioneer has no persistence requirement for admission or controls. Optional context.configStore must be an actual NoSqlConfigStore with immutable scope matching the admitted home and pioneer; structural scope objects are rejected. The borrowed store, database and encryption provider remain caller-owned, and Pioneer performs no gratuitous config read/write. Returned device/entity IDs include the exact encoded home ID and receiver UDN. Executable config, injected clients/executors and static snapshots cannot configure a native runtime.
PioneerConfigFlow.start(candidate, { homeId }) captures the context before returning its form. Home has no editable field; an explicitly conflicting submitted home is refused before receiver I/O, and completed config uses the captured host home. A flow created by the integration shares the owning integration's capacity and cleanup ledger. Ordinary admission errors return an error step; cleanup failures reject and remain owned for explicit cleanup retry.
Features are power, muted, volume_raw and input. Volume is the vendor raw integer range 0 through 185, limited by the required owner maximumVolumeRaw; zero preserves the minimum sentinel. The Pioneer-authored FY16 control sheet has conflicting dB examples, so no dB, normalized fraction or percentage conversion is inferred. Input selection requires a documented physical SC-95 source actually observed by the foundation. Every operation refreshes selected identity, and controls preflight readable receiver settings.
media_player services require the exact device or entity target: turn_on/turn_off with empty data, volume_mute with boolean is_volume_muted, set_volume_raw with integer volume_raw, and select_source with an observed two-digit source. Relative volume, toggles, arbitrary ASCII commands, additional zones, generic model families, media playback and inferred source names are unsupported. Explicit PioneerOfflineMapper methods keep historical values available for preview while forcing online, entity availability, feature writability and local control capability false, even for supplied snapshots containing stale live flags. They never create a native runtime.
Receipts preserve dispatch: 'written', effectOutcome: 'observed-target' | 'indeterminate', commandAcknowledged: false and causalityConfirmed: false. A matching readback is an independent observation; it does not prove this command caused an acoustic or electrical effect. Transport loss or cancellation after dispatch is indeterminate. Effects are submitted once, with no replay, reconnect or transport retry.
The foundation owns bounded FIFO, deadlines, framing and terminal TCP cleanup. The shared native owner bounds pending setup/submission work, live runtimes and retained failed cleanup together, with default capacity 16 and optional constructor capacity. Each accepted client is registered immediately after construction and before startup. Runtime destruction closes its ownership lease; integration destruction closes the whole owner and joins setup, flow, clients and callbacks. A failed cleanup continues occupying capacity until explicit integration.retryCleanup() or flow.retryCleanup() succeeds. These methods retry terminal cleanup only and cannot replay receiver operations or reopen a closed owner.
runtime.subscribe() emits completed explicit read/control observations without background polling. Up to 32 subscriptions are admitted; unfinished callback work is bounded by maxPendingOperations, registered before invoking user code and joined on terminal cleanup. Reentrant abort or destruction shares the already-published close promise. Callback rejection is surfaced by disposal/cleanup. Stale flow submissions and all closed native runtimes are terminal. The SC-95 manual, printed page 58 documents configurable control ports and Network Standby; configure the receiver beforehand. Fixtures exercise loopback HTTP/TCP peers; physical receiver and firmware behavior remain unverified.
Selected Panasonic Viera plaintext NRC
PanasonicVieraIntegration admits one explicitly selected Panasonic NRC endpoint through @ecobridge.xyz/devicemanager's public PanasonicProtocol. Supply homeId, private literal IPv4 host, exact HTTP descriptionUrl, expectedUdn (UPnP UUID), expectedModelName, and allowUnencryptedLan: true. Identity and current service/SCPD metadata are rechecked before every key dispatch. LAN metadata is unauthenticated; the identity pin is not cryptographic hardware authentication.
Only currently advertised keys from the owning protocol's finite subset are exposed. remote_key is a write-only media feature whose value is an admitted key string; button entities have unknown (null) state. Use exact-device panasonic_viera.press or exact-button button.press. The result is the matching SOAP acknowledgement receipt; it never implies the resulting TV power, volume, mute, input, playback, or physical effect. There is no retry or replay. Protocol rejection and indeterminate outcomes remain typed owning-package errors.
Encrypted/PIN families, pairing, absolute rendering settings, media playback, arbitrary SOAP, executor injection and snapshot-backed native setup are refused. The separately named pure PanasonicVieraMapper.toSnapshotFromRaw utility marks supplied data unavailable and read-only, and cannot create a live runtime.
Every setup/config-flow call requires independently selected context.homeId; optional constructor binding cannot supply it. The public PanasonicVieraClient options require the same native home context. An optional store must be an actual NoSqlConfigStore matching the admitted home and panasonic_viera domain. Protocol-only operation requires no store. This keys-only integration does not write that store or close caller storage/database resources. Client, runtime, integration and config-flow destruction are terminal and drain admitted logical work even when cleanup fails; parent setup cancellation closes the owned protocol. Cleanup errors remain observable, multiple cleanup errors are aggregated, and failed allocated handles remain owned without automatic close retries or reopening. Config-flow destruction also fences stale submit closures.
Fixtures qualify the protocol contract only. No physical television or captured model fixture was used. The Panasonic-authored TV Remote 2 guide confirms same-LAN control and model variance (https://av.jpn.support.panasonic.com/support/global/cs/tv/vremote2/ios/); the exact wire subset is documented by the owning foundation's primary implementation-source evidence, not a Panasonic-authored SOAP specification.
Integration and child flow share the bounded native owner, default16 including pending work, live runtimes and retained failed cleanup. Registering clients before startup keeps failed allocations reachable. Terminal destroy publishes its memo before abort and preserves its original failure; only explicit retryCleanup() retries teardown without reopening or repeating keys. Flow captures host context before returning its form and never lets an editable field select home.
Native Modbus TCP integration
The native adapter consumes the published @ecobridge.xyz/devicemanager@4.7.0 ModbusProtocol. It requires an explicit host-selected context.homeId, matching configuration home, private/link-local/loopback IPv4 address, unit 1–247 or direct TCP unit 255, exact expected basic vendor/product identity, explicit unencrypted-LAN acceptance and one through 64 owner read mappings. No endpoint, unit, register meaning or capability is discovered or inferred. The optional expected revision pins that identity object too.
const integration = new ModbusIntegration();
const runtime = await integration.setup({
homeId: 'home-123',
address: '192.168.1.50',
unitId: 1,
expectedVendorName: 'Owner verified vendor',
expectedProductCode: 'Owner verified product',
allowUnencryptedLan: true,
mappings: [
{ id: 'raw-status', name: 'Raw input register', kind: 'input', address: 0 },
{ id: 'raw-output', name: 'Raw coil', kind: 'coil', address: 7, write: true },
{ id: 'raw-setting', name: 'Raw holding register', kind: 'holding', address: 11,
write: { minimum: 10, maximum: 200 } },
],
}, { homeId: 'home-123' });
try {
const entities = await runtime.entities();
await runtime.callService?.({
domain: 'modbus',
service: 'write_coil',
target: { entityId: entities.find((entity) => entity.name === 'Raw coil')!.id },
data: { value: false },
});
} finally {
await integration.destroy();
}
Mappings use zero-based wire addresses and unsigned 16-bit raw registers. Read kinds are coil, discrete_input, holding and input. Each has an explicit ID, display name and address; quantity defaults to one and is bounded by the protocol to 2000 bits or 125 registers. IDs must be unique and ranges of the same kind must not overlap. Multiple values remain raw arrays in entities and individual raw sensor features in device definitions. There is no scaling, signed/float decoding, 40001 notation conversion, default unit, temperature/energy meaning or light capability.
Writes default to denied. A coil mapping with write: true permits one absolute boolean FC5 write. A holding mapping with inclusive write: { minimum, maximum } bounds permits one unsigned integer FC6 write. Both require quantity one. Read-only input mappings, arbitrary addresses, multi-address writes, toggles, retries and replay are rejected. Raw services use domain modbus, service read, write_coil or write_register, and one exact mapped entity ID; writes accept only data: { value }. Device feature writes additionally verify exact home/device provenance and the copied owner permission.
Every read/write freshly checks mandatory basic identity through the owning protocol before dispatch. Identity strings and endpoint/unit are plaintext LAN selection, without cryptographic authentication or unique serial identity. A write receipt confirms the correlated Modbus echo and always reports physicalEffectConfirmed: false; it neither changes observed state nor proves the physical result. Explicit reads provide new observations. Subscriptions are passive and never start a poll timer. Client construction is inert; setup/config admission obtains live mapped reads before succeeding.
timeoutMs (default 5000, maximum 60000) applies to each protocol operation including queue time. The native FIFO defaults to 16 admitted operations and permits a maximum of 128. Cancellation/deadlines and malformed, mismatching or failed transport follow the owning protocol's terminal rules. Failed writes retain conservative not-sent or indeterminate outcomes. The adapter never recreates or replays a terminal client. Shutdown stops admission, aborts owned operations and joins every handle before reporting cleanup errors, including handles whose admission cleanup failed.
Runtime persistence is optional. If supplied, the context store must be a caller-owned encrypted NoSqlConfigStore with the exact homeId and integrationDomain: 'modbus'. Config admission stores the verified selection through that store; the adapter does not close it. No filesystem persistence or database-driver bypass exists.
The integration and its config flow share native home admission and terminal resource ownership. Constructor { homeId } is an optional consistency constraint; it never replaces explicit context home. The first valid context binds the instance synchronously, including when later transport setup fails. A config flow captures its context before returning a form, omits editable home, rejects a conflicting submitted home, and returns the captured home. Configuration, discovery and store scope cannot select it. This is a trusted embedding-host contract, not a home string used as authentication.
Shared ownership defaults to 16 pending setups, runtimes or retained cleanup resources; { capacity } sets that bound. It is separate from each protocol's FIFO. Cleanup failures remain owned and count toward capacity. retryCleanup() retries teardown explicitly, without opening a connection or replaying commands. Closing remains terminal and keeps its stable promise.
Wire references: Modbus Application Protocol V1.1b3, sections 6.1–6.6 and 6.21, and Messaging Implementation Guide V1.0b, MBAP and transaction matching. Qualification is loopback-fixture based; actual hardware, vendor register maps and Modbus Security/TLS are unverified.
Selected Bose SoundTouch
SoundtouchIntegration uses the published @ecobridge.xyz/devicemanager SoundTouch protocol for one selected private IPv4 endpoint, exact uppercase twelve-digit unicast deviceID and exact /info type. It requires explicit allowUnencryptedLan: true, an integer volumeLimit from 0 through 100, and a separately selected host context.homeId. Optional constructor home binding is an additional consistency check; it never replaces the host context. Setup and the integration's config flow share a home binding. An optional store must be an actual NoSqlConfigStore with the same home and soundtouch domain; it remains caller-owned. No store is required for these local operations.
const integration = new SoundtouchIntegration();
const runtime = await integration.setup({
homeId: 'home-one',
host: '192.168.1.90',
deviceId: 'D05FB8A9591C',
type: 'SoundTouch 20',
allowUnencryptedLan: true,
volumeLimit: 40,
}, { homeId: 'home-one' });
try {
const device = (await runtime.devices())[0];
const result = await runtime.callService!({
domain: 'soundtouch', service: 'set_volume',
target: { deviceId: device.id }, data: { volume: 25 },
});
// result.success is false: status STRING has no defined success vocabulary.
// result.data retains status-received acknowledgment, independent readback,
// effectOutcome: 'indeterminate' and causalityConfirmed: false.
} finally {
await integration.destroy();
}
Read services soundtouch.status, .snapshot and .refresh return truthful sanitized observations. Controls are soundtouch.set_volume or media_player.volume_set with integer { volume }, and soundtouch.set_mute or media_player.volume_mute with explicit { muted }. Every service requires an exact owned device or speaker entity target. Canonical feature writes support only volume and mute. Observations retain the vendor's raw 0–100 range, including readings above the owner ceiling; ownerWriteVolumeRange describes the separate write policy. Optional config.name is a local display label; the snapshot's name and device metadata's observedName retain /info name.
The protocol owns final fresh identity, settled-volume and prior-mute checks before every POST. Reads above the ceiling and absolute lowering into range are permitted without clamping. Mute preserves the fresh volume and rejects a preserved target above the ceiling; unmute above the ceiling and increases while freshly muted reject. Lower while preserving mute, then explicitly unmute after a new in-range observation. Concurrent controllers can race after observation; no transaction, compare-and-set, acoustic limit or physical-effect guarantee is claimed.
Manual/custom discovery descriptors only match explicitly selected endpoint identity; they issue no discovery requests. The config flow captures the host home and selected endpoint/type/deviceID before effects, requires plaintext acknowledgment and an owner ceiling, and returns config without persistence. Public SoundtouchClient(config, { homeId }) also requires a separate native home context before allocating its protocol. Unsupported operations include injected snapshots/clients/executors, mDNS discovery, power/standby/remote keys, sources/presets/media/zones, cloud access, retries/replay/reconnect and vendor push subscriptions.
Subscriptions emit completed adapter observations only. Callback failures are retained and surfaced by unsubscribe/terminal cleanup. The shared native owner bounds pending setups, flow submissions, runtimes and retained failed cleanup resources to 16 by default. Client operations default to 16 pending operations with a single 5000 ms deadline covering FIFO waiting and protocol exchanges. Closing is terminal, idempotent and joins owned operations/resources. integration.retryCleanup(), flow.retryCleanup() or closed-client retryCleanup() explicitly retry retained cleanup; they never repeat transport observations or controls. Explicit cleanup retry also acknowledges retained subscriber failures. Borrowed stores, databases and key providers are never closed.
Verified with genuine loopback HTTP fixtures, strict TypeScript with library checks, compiled-source import and Docker limited to two CPUs and 4 GiB with networking disabled. Physical hardware, firmware compatibility and acoustic effects remain unverified. Plaintext deviceID/type matching does not cryptographically authenticate the peer.
Selected Sony BRAVIA Professional display
BraviatvIntegration consumes the public SonyBraviaProtocol from @ecobridge.xyz/devicemanager. It admits a selected BRAVIA Professional version 1.0 REST endpoint by exact model and active-network MAC, with an optional exact serial check. Missing or unusable active-network MACs cannot establish selection. Discovery records supply setup hints only.
import { BraviatvIntegration } from '@smarthome.exchange/integrations';
async function useSelectedDisplay(resolvePrivatePsk: (credentialRef: string, signal: AbortSignal) => Promise<string>) {
const homeId = 'my-home';
const integration = new BraviatvIntegration({
homeId,
resolvePsk: ({ credentialRef, signal }) => resolvePrivatePsk(credentialRef, signal),
});
try {
const runtime = await integration.setup({
homeId,
host: '192.168.1.50',
expectedModel: 'FW-55BZ35F',
expectedMacAddress: '04:5D:4B:AD:6D:E1',
credentialRef: 'my-display-private-psk',
allowUnencryptedLan: true,
volumeLimitRaw: 40,
}, { homeId });
const device = (await runtime.devices())[0];
return await runtime.callService!({
domain: 'braviatv', service: 'set_speaker_volume_raw',
target: { deviceId: device.id }, data: { volume_raw: 20 },
});
} finally {
await integration.destroy();
}
}
The host must supply an independent nonempty context.homeId for every setup and config flow. Constructor homeId is an optional consistency check; config, discovery, form values and stores cannot select the host home. The first admitted host home remains latched even after setup fails. The required constructor resolvePsk is caller-owned. A constructor with only { resolvePsk } is supported; the optional homeId in the example adds a consistency check. Configuration and completed config flows carry a credential reference only. The caller owns private-level PSK storage and retrieval; the adapter never logs, returns, caches or persists the credential. An optional actual caller-owned NoSqlConfigStore must have the exact canonical homeId and braviatv domain; no store is required for control. Structural stand-ins are rejected. The adapter does not write or close it, its database or its key provider. There is no filesystem persistence fallback.
Explicit allowUnencryptedLan: true acknowledges HTTP on a selected private IPv4 address. PSK authorization does not prove peer identity or protect plaintext traffic. The integration checks freshly reported model and active-network MAC before each effect and during independent readback; this is an exact selection check, not cryptographic peer authentication.
Only three separate absolute services are admitted, each with an exact device or corresponding entity target: braviatv.set_power with { power: boolean }, braviatv.set_device_mute with { muted: boolean }, and braviatv.set_speaker_volume_raw with { volume_raw: integer }. Feature writes use power, device_mute and speaker_volume_raw. Power and mute map to switch entities; speaker volume maps to a number entity. Mute applies at device level and observes speaker mute. Volume targets the speaker and requires its freshly advertised integer range plus the caller's volumeLimitRaw ceiling in 0..100 raw levels. Entity minimum/maximum preserve the display’s actual reported raw range; writable entities separately expose maximumWritableRaw limited by the owner ceiling. Read-only entities omit that writable cap. No percentage conversion or clamping occurs.
The facade refreshes supported v1.0 HTTP methods, identity and relevant state before every effect. Results preserve commandAcknowledged, distinguish matching-reported-state from different-reported-state, and keep causalityConfirmed: false. Reported state does not establish physical output or causality. Post-dispatch timeout, connection loss or changed identity remains indeterminate and never replays. Errors omit credentials, device debug strings and caller abort reasons. There is no arbitrary RPC, pairing, IR, toggle, relative volume, media URL, playback or wake fallback. A suspended display must remain reachable through Sony's documented Sleep/Remote start configuration.
timeoutMs defaults to 5000 and is bounded to 1..30000. The deadline starts at FIFO admission and covers queue time, credential resolution, facade preflight, single dispatch, independent readback and transport drain. Constructor capacity defaults to 16 and bounds shared pending setup/flow admissions, runtimes and retained failed cleanup resources. Client maxPendingOperations separately defaults to 16, bounded to 1..128, and includes retained failed facades. Responses default to 32768 bytes, bounded to 1024..65536. Late credential completion after cancellation cannot construct a facade or dispatch. Terminal memoized destroy() aborts and joins owned transports, queued work, failed factories and runtimes even when another cleanup fails. Failed cleanup remains owned. Only explicit retryCleanup() retries cleanup; it never retrieves a credential, constructs a facade or repeats a command. The original destroy() promise remains stable. Closing a config-flow child is terminal for that flow and leaves sibling runtimes usable; closing the integration joins all children and runtimes. Caller abortSignal also closes promoted runtimes. Caller credential providers receive an AbortSignal and monotonic deadline and own their retrieval resources. Destroy joins the actual admitted provider promise even after the request deadline. Rejection with the exact admitted signal.reason remains cancellation; an unrelated late provider rejection becomes a sanitized retained cleanup failure. Explicit retryCleanup() acknowledges that settled failure without invoking the resolver or replaying an effect; the original destroy promise keeps its result. Providers must cooperatively settle their admitted work.
BraviatvClient directly requires (config, { homeId, abortSignal?, configStore? }, { resolvePsk, homeId? }); constructor options never replace that context. BraviatvConfigFlow({ resolvePsk, homeId?, capacity? }) likewise requires an independent homeId when started. Config flows capture the host home and discovery endpoint hint before returning a form and reject conflicting submitted homes before credentials or HTTP.
Subscriptions publish completed observations only and do not start polling. Relevant control readback replaces earlier observations, so unrelated old fields are not presented with a fresh timestamp. Unconfigured registry descriptors remain inert and cannot set up a runtime without explicit caller options.
Disposable real HTTP fixtures use ephemeral loopback ports and synthetic PSKs; they make no hardware claim. The opt-in BRAVIATV_LOAD=1 case runs only in Docker capped to two CPUs and 4 GiB. Sony's canonical sources are the REST API reference and REST structure and PSK levels.
Kodi selected native HTTP JSON-RPC
Kodi observes a caller-selected literal private, link-local or loopback IPv4 HTTP origin through the published @ecobridge.xyz/devicemanager KodiProtocol. Configuration selects the exact application name and major and explicitly accepts plaintext HTTP. The live JSON-RPC API13 schema and case-sensitive ReadData/ControlPlayback permissions govern admission. Application name/version identify a compatible application, not a unique device; plain HTTP and optional Basic credentials do not authenticate the server cryptographically.
const integration = new KodiIntegration({ homeId: 'home-1' });
const runtime = await integration.setup({
homeId: 'home-1',
origin: 'http://192.168.1.70:8080',
expectedApplicationName: 'Kodi',
expectedApplicationMajor: 21,
allowUnencryptedHttp: true,
}, { homeId: 'home-1', abortSignal });
const devices = await runtime.devices();
// Finish this caller-owned lifetime explicitly.
await integration.destroy();
Setup and config flow require an explicit context.homeId selected by the embedding host. An optional constructor home is an additional consistency constraint and never substitutes for the context. The shared native owner captures and fixes the first admitted host home synchronously, even if later readiness fails. This is a trusted embedding-host contract; the home string is not an authentication credential. Configuration, editable fields, advertisements and store scope cannot select that home. An optional caller-owned configStore must be an actual NoSqlConfigStore with the exact admitted home and integrationDomain: 'kodi'; no store is required for Kodi observation or policy capture. Kodi never reads, writes, deletes, lists, creates or closes borrowed store data/resources. Config flow captures the host home and fixes the candidate endpoint; its form does not expose an editable home. Discovery and flow admission use the public shared validateKodiOrigin policy. An eligible numeric advertised host wins; otherwise the first eligible explicitly advertised numeric address is selected. Invalid or absent numeric endpoints and invalid explicit ports are rejected before form creation or home binding. No DNS resolution or HTTP request runs during discovery or form submission. Setup performs fresh network readiness before returning a runtime. Credentials and snapshot data are rejected in production configuration.
Snapshots produce two readable features: integer application volume and boolean muted. Entity playback state is unknown because the selected foundation does not observe players. Metadata reports selection: 'configured-http-endpoint', uniqueDeviceIdentityVerified: false and cryptographicServerIdentityVerified: false. Device and entity ids encode the home and selected normalized HTTP origin; these are local routing keys, not device identity evidence.
Controls default to disabled. Explicit configuration control: { trustSelectedEndpoint: true, maximumVolume: 30 } permits only absolute integer application volume and explicit boolean mute when the current schema/permissions admit them. Services require an exact owned device or entity target: kodi.snapshot with empty data, kodi.set_volume with { volume: 20 }, and kodi.set_muted or media_player.volume_mute with { muted: true }. writeFeature admits only the canonical media features volume and muted for this selected home/endpoint; supplied writable flags cannot grant authority. Percentage-level conversion, volume steps, toggle, playback, media URLs, remote keys, library, power and arbitrary methods are unsupported.
The owner ceiling limits this caller's admitted volume and refuses unmute while the observed volume exceeds it. It does not constrain concurrent controllers or physical audio output. A control result preserves the requested value, actual correlated RPC return and later actual observation separately, with physicalEffectConfirmed: false. A failure after dispatch reports effectOutcome: 'indeterminate'; the adapter never retries or replays it.
Optional Basic credentials are supplied by new KodiIntegration({ homeId, getBasicCredentials }) or new KodiClient(config, { homeId, abortSignal, configStore }, { getBasicCredentials }). The public client requires an independently selected native home context before protocol allocation; credentials stay in its separate third options argument. The private caller-owned callback receives an AbortSignal, runs once per admitted operation and must honor cancellation. Its secret storage and external resources remain caller-owned. Credentials or remote error messages/data are not included in diagnostics or mapping.
The shared native owner bounds pending setups, admitted flow operations, runtimes and retained failed cleanup together to 16 owned slots. The public foundation separately serializes each client's operations in a bounded FIFO, including the active operation: default 16, configurable 1–128. Deadlines start at protocol admission: default 5000 ms, configurable 1–60000. Bodies default to 65536 bytes, configurable 1024–1048576. Queued cancellation removes queued work; active cancellation, deadline/HTTP/transport/correlation/body/selection failures close the client terminally and drain queued work. destroy() installs its stable promise before cancellation, rejects future setup/reads/writes, closes selected sockets and joins admitted providers and flow submissions. Cleanup failures remain retained and observable; retryCleanup() explicitly retries cleanup without replaying protocol operations. Closing the config flow is terminal for that child scope and preserves a separately owned runtime.
KodiMapper.toDevices() and .toEntities() preserve pure conversion of imported offline IKodiSnapshot data. Such outputs always report unavailable/read-only with runtimeAuthority: 'offline-import' and cannot configure or authorize native operation. No application filesystem persistence or Home Assistant bridge is used.
Verification: actual published devicemanager 4.8.0, 55 passing Kodi tests with real loopback HTTP fixtures, genuine strict: true / noImplicitAny: true / skipLibCheck: false source/test checking, and narrow production emit. Docker qualification with 2 CPUs, 4 GiB and no external network drained 128 admitted protocol operations, refused 10000 excess admissions, and drained all 16 admitted setup providers. No physical Kodi hardware or production installation was tested.