Docs

Quick start

Scaffold an integration, build it, run it against a development hub, and publish.

Scaffold

npm i -D @novon/integration-sdk esbuild socket.io-client typescript
npx novon-integration init my-thing --domain x_acme_thing --name "Acme Thing"
cd my-thing && npm install

The CLI writes manifest.json, src/index.ts, tsconfig.json and package.json from its template. Domains are x_<vendor>_<name>: lowercase letters, digits and single underscores, unique across the registry.

Build

npx novon-integration build            # → dist/integration.js + dist/manifest.json

The CLI bundles src/index.ts with esbuild and validates the manifest with the same schema the registry and the hub use.

Run against a development hub

npx novon-integration dev --watch      # builds into ./.novon-dev-integrations/x_acme_thing
NODE_ENV=dev NOVON_DEV_INTEGRATIONS_DIR=$PWD/.novon-dev-integrations COMMUNITY_INTEGRATIONS_ALLOW_UNSIGNED=1 npm run start:edge-runtime

The integration appears in the app's Add integration picker for that hub. Bundles load at runtime boot, so restart the runtime after a rebuild.

Publish

Needs an application API key with the communityIntegration:publish scope (minted in the admin console under Settings → API keys):

export NOVON_ADMIN_API_KEY=nvk_…        # or ~/.config/novon/admin-api-key
npx novon-integration publish --api-url https://stage-admin-api.novon.com --notes "first release"

A (domain, version) is published once and is immutable: bump version in manifest.json for every change. See Publishing.

A minimal integration

src/index.ts
/// <reference types="@novon/integration-sdk/globals" />
import type { IntegrationDefinition } from '@novon/integration-sdk';

const definition: IntegrationDefinition = {
  async setupEntry(ctx) {
    const host = String(ctx.entry.data.host);
    const first = await novon.http(`http://${host}/status`).catch(() => null);
    if (!first?.ok) throw ctx.notReady(`Cannot reach ${host}`);   // hub retries with backoff

    await ctx.entities.add([{
      uniqueId: `${host}:power`, platform: 'switch', name: 'Power', state: 'off',
      device: { uniqueId: host, manufacturer: 'Acme', model: 'Thing' },
      async onService(service) {
        await novon.http(`http://${host}/power`, { method: 'POST', body: { on: service === 'turnOn' } });
        ctx.entities.update(`${host}:power`, { state: service === 'turnOn' ? 'on' : 'off' });
      },
    }]);

    setInterval(async () => {
      const res = await novon.http(`http://${host}/status`);
      ctx.entities.update(`${host}:power`, { state: res.json<{ on: boolean }>().on ? 'on' : 'off' });
    }, 30_000);
  },
};

novon.defineIntegration(definition);

The /// <reference types> line gives you novon, console and the timers without importing anything; the bundle must not import at run time because the hub injects those globals.

On this page