Skip to content
Stand with Ukraine flag

Lesson 3. Device States & Telemetry

This lesson adds synthetic telemetry via a rule chain, builds a colorful sensors table, configures time-window aggregation for consumption metrics, creates device-type states (air, energy, water), and populates each with custom visualization cards and charts.


ThingsBoard rule chains can generate synthetic telemetry via Generator nodes — useful for demos, prototyping, and testing dashboards before real hardware is connected.

Create a new rule chain:

  1. Go to Rule chains and click + Add rule chain ⇾ Create new rule chain.
  2. Name it Device Telemetry Emulators and click Add.
  3. Open the rule chain by clicking on it.

Add four generator nodes (one per device) and a save time series sink to post telemetry every 10 minutes.

  1. Find the generator node in the node palette and drag it onto the canvas.
  2. Name it Indoor air quality data emulator. Set Message count to 100 and Period (seconds) to 600.
  3. Set Originator to device SD-001 (Indoor Air Quality Sensor).
  4. Paste the generator script (see below), then click Add.
    var temperature = toFixed(Math.random() * 10 + 18, 2);
    var humidity = toFixed(Math.random() * 15 + 40, 2);
    var co2 = toFixed(Math.random() * 70 + 440, 2);
    var msg = { temperature: temperature, humidity: humidity, co2: co2 };
    var metadata = { data: 40 };
    var msgType = 'POST_TELEMETRY_REQUEST';
    return { msg: msg, metadata: metadata, msgType: msgType };
  5. Add a second generator node named Power consumption data emulator. Set count 100, period 600, originator EM-002.
  6. Paste the energy script, then click Add.
    var powerConsumption = toFixed(Math.random() * 2.2, 2);
    var msg = { powerConsumption: powerConsumption };
    var metadata = { data: 40 };
    var msgType = 'POST_TELEMETRY_REQUEST';
    return { msg: msg, metadata: metadata, msgType: msgType };
  7. Add a third generator node named Water consumption data emulator. Set count 100, period 600, originator WM-003.
  8. Paste the water script, then click Add.
    var waterConsumption = toFixed(Math.random() * 1.3, 2);
    var batteryLevel = toFixed(Math.random() * 1 + 45, 2);
    var msg = { waterConsumption: waterConsumption, batteryLevel: batteryLevel };
    var metadata = { data: 40 };
    var msgType = 'POST_TELEMETRY_REQUEST';
    return { msg: msg, metadata: metadata, msgType: msgType };
  9. Add a fourth generator node named IAQ data emulator. Set count 100, period 600, originator AM-307.
  10. Paste the IAQ script, then click Add.
    var temperature = toFixed(Math.random() * 10 + 18, 2);
    var humidity = toFixed(Math.random() * 15 + 40, 2);
    var co2 = toFixed(Math.random() * 70 + 440, 2);
    var msg = { temperature: temperature, humidity: humidity, co2: co2 };
    var metadata = { data: 40 };
    var msgType = 'POST_TELEMETRY_REQUEST';
    return { msg: msg, metadata: metadata, msgType: msgType };
  11. Drag a save time series node onto the canvas and click Add.
  12. Connect the Success output of each Generator to the Save time series node.
  13. Save the rule chain.

To verify the rule chain is working, open any device’s Latest telemetry tab — you should see values arriving within the first 600-second interval.


Step 3.2 Sensors List — Display Telemetry

Section titled “Step 3.2 Sensors List — Display Telemetry”

Update the Office sensors list widget: add all telemetry keys, hide the raw columns, then combine readings into one styled Telemetry column using a Cell content function.

Add telemetry keys:

  1. Open the office state, enter edit mode, and click the pencil icon on the Office sensors list widget.
  2. Add the following data keys: temperature, humidity, co2, powerConsumption, waterConsumption.
  3. Click Apply to save, then close the widget editor.

Devices can send multiple telemetry values. For example, an Indoor Air Quality Sensor reports temperature, humidity, and CO2 level.

By default, each telemetry key appears in a separate table column. To make the table cleaner, combine related telemetry values into a single column and hide unnecessary columns.

Add the combined telemetry column:

  1. Re-open the widget editor.

  2. Click the Time series icon to add a new key and name it telemetryValue.

  3. Rename the column label to something like Telemetry.

  4. Click the gear icon next to the telemetryValue row, enable Use cell content function, and paste the following:

    if (entity.temperature && entity.humidity && entity.co2) {
    return '<div style="display:flex;flex-direction:row;flex-wrap:wrap;gap:7px;margin:10px 0;align-items:center">' +
    '<div style="padding: 4px 12px;background:#e0e0e0;border-radius:20px;">Temp: ' + entity.temperature.toFixed(0) + ' °C</div>' +
    '<div style="padding: 4px 12px;background:#e0e0e0;border-radius:20px;">Hum: ' + entity.humidity.toFixed(0) + ' %</div>' +
    '<div style="padding: 4px 12px;background:#e0e0e0;border-radius:20px;">CO2: ' + entity.co2.toFixed(0) + ' ppm</div>' +
    '</div>';
    } else if (entity.powerConsumption) {
    return '<div style="display:inline-block;padding: 4px 12px;background:#d3d3d3;border-radius:20px;">' + entity.powerConsumption.toFixed(1) + ' kW</div>';
    } else if (entity.waterConsumption) {
    return '<div style="display:inline-block;padding: 4px 12px;background:#d3d3d3;border-radius:20px;">' + entity.waterConsumption.toFixed(1) + ' gal</div>';
    }
    return value;
  5. Click Save.

Hide the raw columns:

  1. Click the gear icon next to each of the five raw keys (temperature, humidity, co2, powerConsumption, waterConsumption).
  2. Set Default column visibility to Hidden for each one.
  3. Click Save after each change.

Set aggregation for consumption keys:

  1. Click the pencil icon next to powerConsumption and set Aggregation to Sum.
  2. Do the same for waterConsumption.
  3. Remove any auto-generated prefixes from column labels if needed.
  4. Enable Use dashboard time window for the widget.
  5. Click Apply, then save the dashboard.

Step 3.3 Configure the Dashboard Time Window

Section titled “Step 3.3 Configure the Dashboard Time Window”

Meters emit rates (kW, gallons/min), not totals. Configure the dashboard time window to use Sum aggregation in history mode and appropriate grouping intervals (1 hour for rates → totals per hour).

  1. Enter dashboard edit mode and click the Edit time window icon on the toolbar.
  2. Click the gear icon to open the time window settings.
  3. On the Realtime tab:
    • Last interval: hide this option from users.
    • Relative tab: click the pencil icon and uncheck all intervals except Current day, Current week (Sun–Sat), and Current week (Mon–Sun). Set their grouping interval and default grouping interval to 1 hour.
    • Check Current month and set its grouping to 2 hours.
    • Click Apply.
  4. On the History tab:
    • Hide the Last and Range interval options.
    • Set Aggregation to Sum and hide the aggregation selector.
    • Set grouping interval to 1 hour and lock it (prevent users from changing it).
    • Click Apply.
  5. Click Update to apply the time window settings, then save the dashboard.

Add three dashboard states — one for each device type. Clicking a row in the sensors list or a marker in the Image Map will navigate to the matching state.

  1. Enter edit mode and click Manage dashboard states ⇾ +.
  2. Add state: name ${entityName}, ID air_sensor. Click Add.
  3. Add state: name ${entityName}, ID energy_sensor. Click Add.
  4. Add state: name ${entityName}, ID water_sensor. Click Add.
  5. Click Save.

Step 3.5 Sensors List — Navigation to Device States

Section titled “Step 3.5 Sensors List — Navigation to Device States”

Each device type has its own state (air_sensor, energy_sensor, water_sensor). Use a Custom action with on-row-click that reads the device type and routes to the correct state.

  1. In the office state (edit mode), click the pencil icon on the Office sensors list widget.

  2. Scroll to Actions and click Add action ⇾ +.

  3. Set source to On row click and enter an action name (e.g. go_to_device).

  4. Set action type to Custom action.

  5. Paste the following function:

    const $injector = widgetContext.$injector;
    const deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
    deviceService.getDevice(entityId.id).subscribe(device => {
    if (device.type === 'energy-sensor') {
    openDashboardState('energy_sensor');
    } else if (device.type === 'water-sensor') {
    openDashboardState('water_sensor');
    } else {
    openDashboardState('air_sensor');
    }
    });
    function openDashboardState(stateId) {
    const params = {
    entityId: entityId,
    entityName: entityName
    };
    widgetContext.stateController.openState(stateId, params, false);
    }
  6. Click Add, review the action, then click Save.

  7. Click Apply, then save the dashboard.


Step 3.6 Image Map — Telemetry Tooltips and Navigation

Section titled “Step 3.6 Image Map — Telemetry Tooltips and Navigation”

Update the Image Map widget: add telemetry keys and a custom tooltip showing device-specific readings, then add a Tooltip tag action with the same routing logic as the sensors list.

Add telemetry keys to the Image Map:

  1. In the office state (edit mode), click the pencil icon on the Image Map widget.
  2. In the Overlays -> Marker open marker configuration.
  3. Add additional data keys to the existing data source: temperature, humidity, co2, powerConsumption, waterConsumption, batteryLevel, deviceType.
  4. Edit the Tooltip:
    • Enable Use tooltip function
    • Paste the following tooltip function:
    let tooltip = '<h1 style="margin:8px 0;width:200px;border-bottom:1px solid #0000000d;padding-bottom:8px;padding-right:15px;font-size:16px;font-weight:600;line-height:24px;">${entityLabel}</h1>';
    let typeTooltip = '';
    if (data.deviceType == 'energy-sensor') {
    typeTooltip =
    '<div style="display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:10px;">' +
    '<span style="font-weight:600;font-size:12px;color:#0000008a;max-width:90px;">Power consumption: </span>' +
    '<span style="font-weight:600;font-size:13px;color:#000000de">${powerConsumption:1} kW</span>' +
    '</div>';
    typeTooltip += '<div style="margin-top:17px;text-align:center;background:var(--tb-primary-50,#87CEEB);border-radius:6px;"><link-act name="sensor_details">Details ></link-act></div>';
    } else if (data.deviceType == 'air-sensor') {
    typeTooltip =
    '<div style="display:flex;flex-direction:column;">' +
    '<div style="display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:10px;">' +
    '<span style="font-weight:600;font-size:12px;color:#0000008a;max-width:90px;">Temperature: </span>' +
    '<span style="font-weight:600;font-size:13px;color:#000000de">${temperature:0} °C</span>' +
    '</div>' +
    '<div style="display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:10px;">' +
    '<span style="font-weight:600;font-size:12px;color:#0000008a;max-width:90px;">Humidity: </span>' +
    '<span style="font-weight:600;font-size:13px;color:#000000de">${humidity:0} %</span>' +
    '</div>' +
    '<div style="display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:10px;">' +
    '<span style="font-weight:600;font-size:12px;color:#0000008a;max-width:90px;">CO2: </span>' +
    '<span style="font-weight:600;font-size:13px;color:#000000de">${co2:0} ppm</span>' +
    '</div>' +
    '</div>';
    typeTooltip += '<div style="margin-top:17px;text-align:center;background:var(--tb-primary-50,#87CEEB);border-radius:6px;"><link-act name="sensor_details">Details ></link-act></div>';
    } else if (data.deviceType == 'water-sensor') {
    typeTooltip =
    '<div style="display:flex;flex-direction:column;">' +
    '<div style="display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:10px;">' +
    '<span style="font-weight:600;font-size:12px;color:#0000008a;max-width:90px;">Water consumption: </span>' +
    '<span style="font-weight:600;font-size:13px;color:#000000de">${waterConsumption:1} gal</span>' +
    '</div>' +
    '<div style="display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:10px;">' +
    '<span style="font-weight:600;font-size:12px;color:#0000008a;max-width:90px;">Battery Level: </span>' +
    '<span style="font-weight:600;font-size:13px;color:#000000de">${batteryLevel:0} %</span>' +
    '</div>' +
    '</div>';
    typeTooltip += '<div style="margin-top:17px;text-align:center;background:var(--tb-primary-50,#87CEEB);border-radius:6px;"><link-act name="sensor_details">Details ></link-act></div>';
    }
    return tooltip + typeTooltip;
  1. Set the Tooltip Y offset to -0.77.

  2. Add the tooltip tag action:

    • Click + (Add tag action)
    • Name it sensor_details
    • Set action to Custom action
    • Paste the following function:
    const $injector = widgetContext.$injector;
    const deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
    deviceService.getDevice(entityId.id).subscribe(device => {
    if (device.type === 'air-sensor') {
    openDashboardState('air_sensor');
    } else if (device.type === 'water-sensor') {
    openDashboardState('water_sensor');
    } else {
    openDashboardState('energy_sensor');
    }
    });
    function openDashboardState(stateId) {
    const params = {
    entityId: entityId,
    entityName: entityName
    };
    widgetContext.stateController.openState(stateId, params, false);
    }
  3. Click Add, then Save.

  4. Go to the Widget card tab ⇾ Advanced widget style ⇾ Widget CSS and paste:

    .leaflet-tooltip-pane .leaflet-tooltip-top {
    opacity: 1 !important;
    }
    .leaflet-popup-content {
    width: auto !important;
    margin: 8px;
    }
    a.leaflet-popup-close-button {
    font-size: 20px;
    color: black;
    border-radius: 2px;
    top: 8px;
    right: 5px;
    }
  5. Click Apply, then save the dashboard.

Click a marker to display tooltip with telemetry data and Details button. Click Details > to navigate to the corresponding device dashboard state


Build the air_sensor state with three Indoor Environment cards (temperature, humidity, CO2) and two line charts showing trends.

  1. Open Indoor Air Quality Sensor state.
  2. Click + Add widget in edit mode.
  3. Select Indoor Environment ⇾ Indoor temperature card.
  4. Set datasource to Selected entity (use existing temperature key).
  5. Configure value ranges and colors for Icon and Value sections.
  6. Clear Card border radius and click Add.
  7. Resize the widget as needed.
  1. Click + Add widget and select Indoor Environment ⇾ Indoor humidity card.
  2. Set datasource to Selected entity (use existing humidity key).
  3. Configure value ranges and colors for Icon and Value sections (e.g. normal 40–60 %, warning below 30 % or above 70 %).
  4. Clear Card border radius and click Add.
  5. Place the widget next to Temperature and resize.
  1. Click + Add widget and select Indoor Environment ⇾ Indoor CO2 card.
  2. Set datasource to Selected entity (use existing CO2 key).
  3. Configure value ranges and colors for Icon and Value sections
  4. Clear Card border radius and click Add.
  5. Place the widget to the right of the humidity card, resize, and save the dashboard.
  1. Click + Add widget and select Charts ⇾ Line chart.
  2. Configure time window:
    • Relative ⇾ Current day
    • Average aggregation, 1 hour interval
    • Click Update
  3. Set datasource to Selected entity and add temperature and humidity keys with labels and units.
  4. Configure data keys: Enable Show points and Point label for both
  5. Configure Y-axis:
    • Left: Temperature (°C)
    • Right: Humidity (%) (assign to humidity)
  6. Set title to Temperature and Humidity history.
  7. Configure legend (Bottom, disable Average).
  8. Clear Card border radius and click Add.
  9. Place the widget in the top-right corner, resize, and save the dashboard.
  1. Click + Add widget and select Charts ⇾ Line chart.
  2. Configure time window:
    • Relative ⇾ Current day
    • Average aggregation, 1 hour interval
    • Click Update
  3. Set datasource to Selected entity and add co2 key with label and units.
  4. Configure data key: - Enable Smooth line
  5. Configure chart:
    • Y-axis: CO2 level (ppm)
    • Title: Air Quality
    • Legend: Bottom, enable Min, Max, Average
  6. Clear Card border radius and click Add.
  7. Place the widget below Temperature and Humidity history, resize, and save the dashboard.

Build the energy_sensor state with a power consumption value card (Sum aggregation) and a range chart showing hourly totals.

  1. Open Energy Meter state.
  2. Click + Add widget in edit mode.
  3. Select Industrial widgets ⇾ Power consumption card.
  4. Set datasource to Selected entity (use powerConsumption key).
  5. Configure data key: Set aggregation to Sum
  6. Use the dashboard time window.
  7. Configure widget:
    • Label: Power consumption
    • Set value ranges and colors (Icon and Value)
    • Set font size to 35 px
    • Set unit label to kW
  8. Clear Card border radius and click Add.
  9. Resize the widget and save the dashboard.
  1. Click + Add widget in edit mode and select Charts ⇾ Range chart.
  2. Set datasource to Selected entity and add powerConsumption key.
  3. Configure chart:
    • Title: Power consumption history
    • Units: kW
    • Use dashboard time window
    • Set value ranges and colors
  4. Configure appearance:
    • Disable Label in thresholds
    • Set Line color opacity to 40%
  5. Configure Y-axis:
    • Label: Power consumption
    • Disable Show split lines
  6. Configure card:
    • Disable Data export
    • Clear Card border radius
    • Click Add
  7. Place the widget next to Power consumption, resize, and save the dashboard.

Build the water_sensor state with water consumption (card + chart) and a battery level gauge widget.

  1. Open Water Flow Meter state from Office sensors list.
  2. Click + Add widget in edit mode.
  3. Select Industrial widgets ⇾ Flow rate card.
  4. Set datasource to Selected entity and add waterConsumption key.
  5. Configure data key: Set aggregation to Sum
  6. Use the dashboard time window.
  7. Configure widget:
    • Label: Water consumption
    • Set units to gallon
    • Change icon and configure icon colors
    • Configure value ranges and colors
  8. Clear Card border radius and click Add.
  9. Resize the widget and save the dashboard.
  1. Open Energy Meter state in edit mode and copy Power consumption history widget.
  2. Open Water Flow Meter and paste the widget.
  3. Place it next to Water consumption and resize.
  4. Open widget settings and update:
    • Datasource: Selected entity
    • Key: waterConsumption
    • Title: Water consumption history
    • Units: gal
    • Disable Fill area
    • Set value ranges and colors
    • Y-axis label: Water consumption
  5. Click Apply
  6. Save the dashboard.
  1. Click + Add widget and select Status indicators ⇾ Battery level.
  2. Set datasource to Selected entity.
  3. Keep batteryLevel as the data key.
  4. Clear Card border radius and click Add.
  5. Place next to Water consumption history.
  6. Resize, and save.


Best Practices: Telemetry, Aggregation, and Multi-Level Widgets

Section titled “Best Practices: Telemetry, Aggregation, and Multi-Level Widgets”

Rule Chain Generators — Use generators for demo dashboards and testing. Keep messages counts and periods reasonable (100 messages, 10-min intervals) to simulate realistic data without overwhelming the system.

Cell Content Functions — Use these to combine multiple keys into single formatted columns. Check data type and availability before rendering (avoid null/undefined errors).

Device-Type Routing — When a single action must route to different states based on device type, use Custom actions with getDevice() to read the device’s type field and call openState() accordingly.

Tooltip Functions — Render different telemetry fields per device type. Use template substitution (${powerConsumption:1}, ${temperature:0}) to format values directly in the tooltip HTML.

State Hierarchy & Context — Each state should represent one level of the user’s drill-down journey. Pass context via Entity from dashboard state or Asset search query aliases so widgets adapt to the selected entity automatically.