Expert Guide

Advanced configuration for AI optimizers and complex systems.

Cloud Device Connection

The @larva.io/clouddevice library provides a WebSocket-based connection to Fentrica edge devices through a cloud broker proxy. This enables real-time, bidirectional communication between your application and the physical devices in a building.

Installation

npm install --save @larva.io/clouddevice

Quick Start

import { Device } from '@larva.io/clouddevice';

const device = new Device('deviceId', 'unitId', 'accessToken', {
  server: 'wss://broker.fentrica.com',
  timeout: 8000
});

device.open().then(() => {
  return device.getUINodes();
}).then(nodes => {
  console.log(nodes);
  return device.close();
});

Connection Flow

Create DeviceOpen WebSocketFetch UI NodesListen for EventsRender & Interact
  1. Create Device Instance — Instantiate a Device with the device ID, unit ID, and access token from the Service Connection API response.
  2. Open WebSocket — Call device.open() to establish a persistent WebSocket connection through the Fentrica broker proxy.
  3. Fetch UI Nodes — Call device.getUINodes() to retrieve the device's UI configuration — the list of controllable components (switches, sliders, sensors, etc.).
  4. Listen for Events — Register event listeners for broadcast messages, connection state changes, and errors.
  5. Render & Interact — Pass the UI node configuration to @larva.io/webcomponents for rendering. Handle user interactions via output and request events.

API Reference

Constructor

new Device(deviceId: string, unitId: string, token: string | (() => string | Promise<string>), options?: Options)
ParameterTypeDescription
deviceIdstringDevice ID from the Service Connection API
unitIdstringUnit ID from the Service Connection API
tokenstring | () => string | Promise<string>JWT access token, or a function that returns one (sync or async). See below.
options.serverstringWebSocket broker URL (wss://broker.fentrica.com)
options.timeoutnumberConnection timeout in milliseconds (default: 8000)

Use a token function, not a static string. JWT tokens are short-lived. If you pass a static token string, the connection will fail after the token expires. Pass a function that returns the current valid token so the library can re-authenticate automatically.

// Bad — token will expire
const device = new Device(deviceId, unitId, 'eyJhbG...', options);

// Good — sync function that returns a fresh token
const device = new Device(deviceId, unitId, () => authStore.getAccessToken(), options);

// Good — async function (e.g. refresh token flow)
const device = new Device(deviceId, unitId, () => authService.getValidToken(), options);

Methods

MethodReturnsDescription
open()Promise<void>Opens the WebSocket connection
close()Promise<void>Closes the connection
getUINodes()Promise<UINode[]>Fetches the device UI configuration
request(topic, data?)Promise<any>Sends an RPC request to the device
handleNodeOutput(event)Promise<void>Handles output events from web components
handleNodeRequest(event)Promise<void>Handles request events from web components

Events

EventDescription
openWebSocket connection established
closedConnection closed
broadcastDevice sent a broadcast message (e.g. sensor value update)
errorConnection or communication error
device.addEventListener('broadcast', (event) => {
  console.log('Device broadcast:', event.detail);
});

device.addEventListener('closed', () => {
  console.log('Connection closed');
});

device.addEventListener('error', (event) => {
  console.error('Device error:', event.detail);
});

UI Node Structure

The getUINodes() method returns an array of UI node objects that describe the device's controllable components:

interface UINode {
  node: {
    id: string;      // Unique node identifier
    type: string;     // Component type (e.g. "lar-switch", "lar-slider")
  };
  ui: {
    name: string;         // Display name
    icon?: string;        // Icon identifier
    category?: {          // Grouping category
      name: string;
      icon?: string;
    };
    room?: {              // Room assignment
      name: string;
      icon?: string;
    };
    favorite: boolean;    // User favorite flag
    visible: boolean;     // Visibility flag
    linkednodes?: string[]; // Nested sub-node IDs
  };
}

The node.type field corresponds directly to the web component tag name used by @larva.io/webcomponents. Pass this configuration to the web components to render the device UI.

Connecting Multiple Devices

A single unit (smart space) may have multiple devices. Connect to each device from the API response:

import { Device } from '@larva.io/clouddevice';

// unitDevices from the Service Connection API response
const connections = unitDevices.devices.map(
  (d) => new Device(d.id, unitDevices.id, accessToken, {
    server: 'wss://broker.fentrica.com',
    timeout: 8000
  })
);

// Open all connections
await Promise.all(connections.map((c) => c.open()));

// Fetch UI nodes from all connections
const allNodes = await Promise.all(
  connections.map(async (c) => {
    const nodes = await c.getUINodes();
    return nodes.map((n) => ({ ...n, connection: c }));
  })
);

const nodeList = allNodes.flat();

Keep a reference to the connection alongside each node. When rendering web components, the @output and @request events must be routed back to the correct device connection.

Connection Lifecycle Management

WebSocket connections must be closed when the user leaves the screen or the app goes to the background. Failing to do so wastes resources on the broker and edge device, and can lead to stale connections that silently stop receiving data. When the user returns, re-create all connections from scratch.

This applies to all platforms — browser tabs, mobile web views, and native app shells like Capacitor or Cordova.

Browser (Page Visibility API)

import { Device } from '@larva.io/clouddevice';

let connections: Device[] = [];

async function openConnections(devices: { id: string }[], unitId: string, token: string) {
  connections = devices.map(
    (d) => new Device(d.id, unitId, token, {
      server: 'wss://broker.fentrica.com',
      timeout: 8000
    })
  );
  await Promise.all(connections.map((c) => c.open()));
  return connections;
}

async function closeConnections() {
  await Promise.all(connections.map((c) => c.close()));
  connections = [];
}

// Close when tab becomes hidden, re-create when visible again
document.addEventListener('visibilitychange', async () => {
  if (document.visibilityState === 'hidden') {
    await closeConnections();
  } else {
    // Re-create connections with fresh state
    await openConnections(devices, unitId, token);
  }
});

Capacitor / Cordova (Native App)

For native app shells, use the platform's app state listener instead of the Page Visibility API:

import { App } from '@capacitor/app';
import { Device } from '@larva.io/clouddevice';

let connections: Device[] = [];

App.addListener('appStateChange', async ({ isActive }) => {
  if (!isActive) {
    // App moved to background — close all connections
    await Promise.all(connections.map((c) => c.close()));
    connections = [];
  } else {
    // App returned to foreground — re-create connections
    connections = await openConnections(devices, unitId, token);
  }
});

Vue / React Component Cleanup

Always close connections when the component unmounts:

// Vue 3
onUnmounted(async () => {
  await Promise.all(connections.map((c) => c.close()));
});

// React
useEffect(() => {
  return () => {
    connections.forEach((c) => c.close());
  };
}, []);

Next Steps