Organization

User guide

How to use HorizonMesh

What each part of the console is for, how it works, and step-by-step workflows from organization to SCADA and firmware updates.

Organization

What it is for

An isolated tenant for your fleet: devices, classes, members, quotas, and MQTT broker settings live inside one organization.

How it works

You switch the active organization in the header. Dashboard stats, device catalog, SCADA plans, and notifications are always scoped to the selected org.

Workflow

  1. Register and sign in.
  2. Create an organization or accept an invite.
  3. Select the org in the header — the dashboard shows its fleet summary.
  4. Invite teammates with owner, admin, or member roles.

Examples

Create organization (API body)

POST /v1/organizations — first org is created during signup; additional orgs via API or console.

{
  "name": "Greenhouse Ops",
  "slug": "greenhouse-ops"
}

Organization context in MQTT topics

Every device topic includes the organization UUID, not the slug.

# orgId = UUID from Settings → Organization
to/organization/{orgId}/device/{deviceId}/telemetry/Cap
from/organization/{orgId}/device/{deviceId}/twin/reported

Device class

What it is for

A template for a device type: protobuf schema, twin defaults, remote methods, firmware artifacts, and optional SCADA symbol.

How it works

Every device belongs to exactly one class. Telemetry decoding, twin validation, method calls, and OTA images all come from the class definition.

Workflow

  1. Open Device classes → Create.
  2. Upload or paste the proto schema and define telemetry streams.
  3. Add remote methods and default twin JSON if needed.
  4. Upload firmware binaries and optional SCADA symbol.
  5. Save — new devices of this type inherit the contract.

Examples

Remote methods (device class)

JSON array on the class spec. Timeout is set only when invoking from the UI (10–300 s, default 30).

[
  { "name": "ping", "description": "Health check — returns {"pong":true}" },
  { "name": "reboot", "description": "Reboot device" },
  { "name": "cap_info", "description": "Poll interval and ADC calibration" }
]

Default desired (hm-soil-cap)

Plain JSON (not JSON Schema). New devices inherit these twin defaults.

{
  "cap_state": {
    "cap_interval_ms": 5000,
    "cap_calibration": {
      "dry_raw": 3200,
      "wet_raw": 1200
    }
  }
}

Telemetry chart config (telemetryUi)

Stream name must match the proto message name (e.g. Cap). Field names use camelCase in JSON.

{
  "charts": [
    {
      "id": "soil-moisture-percent",
      "title": "Soil moisture",
      "type": "line",
      "streamFilter": "Cap",
      "timeField": "timestampMs",
      "timeUnit": "ms",
      "series": [{ "field": "moisturePercent", "label": "Moisture %" }]
    }
  ]
}

Device onboarding

What it is for

Secure first connection: a provisioning token lets firmware claim a device slot and receive an mTLS certificate without manual cert handling in the UI.

How it works

You issue a token in the console, embed the claim in firmware, and the device enrolls over Wi‑Fi. mqtt-worker assigns routes; the device appears online in the fleet catalog.

Workflow

  1. Go to Provisioning tokens → create a token for the target class.
  2. Flash the claim into firmware before deployment.
  3. Power the device on a network that reaches your MQTT broker.
  4. Watch the device enroll and pick up its certificate.
  5. Open the device card to verify twin and MQTT topics.

Examples

Provisioning flow (conceptual)

Token is created in the console; firmware embeds claim material before first Wi‑Fi connect.

{
  "provisioning_token": "<secret from console>",
  "device_class": "hm-soil-cap",
  "claim_name": "greenhouse-sensor-01"
}

# After claim, API returns MQTT broker URL, topic roots,
# and the device receives an mTLS client certificate (CN = device UUID).

Devices

What it is for

The fleet catalog: every physical unit with status, class, connection state, and links to twin, telemetry, methods, and settings.

How it works

Devices are created manually or via provisioning. Presence (online/offline) updates from MQTT. The list supports search, status filters, and sorting.

Workflow

  1. Select your organization.
  2. Create a device under the right class or wait for provisioning enrollment.
  3. Open the device overview for MQTT subscribe/publish prefixes.
  4. Use tabs for twin, live log, events, and settings.

Examples

MQTT topic roots (device card)

Subscribe prefix is cloud→device; publish prefix is device→cloud.

# Subscribe (downlink)
to/organization/{orgId}/device/{deviceId}/#

# Publish (uplink)
from/organization/{orgId}/device/{deviceId}/

Invoke direct method (REST)

POST .../devices/{deviceId}/methods/{methodName}/invoke. Does not check twin online/offline — sends to broker and waits until timeout.

{
  "params": {},
  "timeout": "30s"
}

# Example with params (if schema defined on class):
{
  "params": { "dry_raw": 3200, "wet_raw": 1200 },
  "timeout": "60s"
}

Direct method MQTT (platform ↔ firmware)

Correlation id in JSON id field. Platform publishes request; device responds on from/.../response.

# Request (cloud → device)
Topic: to/organization/{orgId}/device/{deviceId}/methods/ping/request
Body:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "payload": {}
}

# Response (device → cloud)
Topic: from/organization/{orgId}/device/{deviceId}/methods/ping/response
Body:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": 200,
  "payload": { "pong": true }
}

Twin (desired & reported)

What it is for

Cloud-to-device configuration (desired) and device-to-cloud state (reported) as JSON documents validated against the class schema.

How it works

You patch desired in the UI; API stores a snapshot and mqtt-worker publishes retained MQTT. Firmware publishes reported; twin-worker merges it into Postgres and updates presence.

Workflow

  1. Open a device → Twin tab.
  2. Edit desired JSON (target temperature, mode flags, etc.) and save.
  3. Device receives retained desired after reconnect if it was offline.
  4. Compare reported side-by-side to confirm the device applied settings.

Examples

Patch desired (UI / API)

Partial or full desired document. mqtt-worker publishes retained MQTT to the device.

{
  "cap_state": {
    "cap_interval_ms": 10000,
    "cap_calibration": {
      "dry_raw": 3100,
      "wet_raw": 1150
    }
  }
}

Reported (device → cloud)

Firmware publishes the applied cap_state. Same shape as desired for hm-soil-cap.

{
  "cap_state": {
    "cap_interval_ms": 10000,
    "cap_calibration": {
      "dry_raw": 3100,
      "wet_raw": 1150
    }
  }
}

MQTT twin topics

Replace {orgId} and {deviceId} with UUIDs from the device card.

# Cloud → device (retained desired diff)
to/organization/{orgId}/device/{deviceId}/twin/desired

# Device → cloud (full reported JSON)
from/organization/{orgId}/device/{deviceId}/twin/reported

Telemetry

What it is for

Live and historical sensor data decoded from protobuf streams defined on the device class.

How it works

Devices publish protobuf telemetry over MQTT. telemetry-worker writes to TimescaleDB and fans out WebSocket updates — charts refresh without polling the REST API.

Workflow

  1. Define telemetry messages in the class proto.
  2. Open the device overview or Live log tab.
  3. Watch the last 15 minutes from storage plus new points in real time.
  4. Expand charts or change the time range for troubleshooting.

Examples

MQTT telemetry topic

Protobuf payload on the wire; UI and rules use decoded JSON field names.

# Publish (protobuf message Cap)
from/organization/{orgId}/device/{deviceId}/telemetry/Cap

Decoded Cap sample (JSON)

What telemetry-worker stores and what event conditions see (expr-lang field names).

{
  "timestampMs": 1710000000123,
  "moistureAdcRaw": 1842,
  "moisturePercent": 42.5,
  "sampleIndex": 128
}

Events & notifications

What it is for

Automated reactions to telemetry or peer conditions — with in-app notifications when rules fire.

How it works

Rules use expr-lang expressions on decoded JSON. rule-worker evaluates streams; matching events create notifications you can read per device or organization.

Workflow

  1. Open a device → Events tab.
  2. Create a rule with a condition (e.g. moisturePercent < 60).
  3. Subscribe to notifications if you want alerts for that device.
  4. Browse Notifications in the sidebar for the org-wide feed.

Examples

Create device event (API body)

POST /v1/organizations/{orgId}/devices/{deviceId}/events

{
  "name": "Low moisture alert",
  "enabled": true,
  "trigger_stream": "Cap",
  "condition": "moisturePercent < 35",
  "action_type": "notification",
  "action_stream": "notification/warning",
  "action_payload": "{\"title\":\"Low moisture\",\"message\":\"{{ index . \"moisturePercent\" }}% at {{ index . \"moistureAdcRaw\" }} ADC\"}",
  "notification_retention_days": 30
}

Condition examples (expr-lang)

Use camelCase JSON field names from telemetry. Empty condition is treated as always true.

moisturePercent < 40
moisturePercent >= 10 && moisturePercent < 25
moistureAdcRaw > 3000

Notification payload template

Go text/template over the telemetry JSON object. Use index . "fieldName" syntax.

{
  "title": "Soil alert",
  "message": "Moisture {{ index . "moisturePercent" }}% (ADC {{ index . "moistureAdcRaw" }})"
}

Class event template (telemetryUi.__eventPresets)

Save a branch as a class template, then add it on another device of the same class and customize.

{
  "__eventPresets": {
    "templates": [
      {
        "id": "tpl-low-moisture",
        "label": "Low moisture",
        "trigger_stream": "Cap",
        "condition": "moisturePercent < 35",
        "action_payload": "{\"title\":\"Low moisture\",\"message\":\"{{ index . \"moisturePercent\" }}%\"}",
        "notif_severity": "warning"
      }
    ]
  }
}

SCADA plans

What it is for

Visual topology of your fleet: class symbols on a canvas, links between devices, and automation on connections.

How it works

Plans reuse device classes and their SCADA symbols. You place nodes, draw links, and configure peer automation without a separate diagram tool.

Workflow

  1. Upload a SCADA symbol on the device class (optional).
  2. Open SCADA → create a plan for your organization.
  3. Drag devices onto the canvas and connect them.
  4. Configure link automation and open inspectors for twin or methods.

Examples

Peer automation on a link (conceptual)

Configured in the SCADA inspector when two devices are connected on the canvas.

{
  "source_device_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "target_device_id": "ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj",
  "condition": "moisturePercent < 30",
  "action_stream": "Cap",
  "action_payload": "{ \"relay\": \"on\" }"
}

Firmware updates

What it is for

OTA delivery of new images published on a device class, with progress reported by devices over MQTT.

How it works

Firmware binaries live on the class. Setting a desired firmware version (or OTA topic) triggers mqtt-worker to push the update; devices download and report status.

Workflow

  1. Upload a new firmware version on the device class.
  2. Create or update devices of that class.
  3. Set the target version in twin or use the class OTA flow.
  4. Monitor firmware status on the device card until the rollout completes.

Examples

Firmware OTA MQTT

Binary is stored on the device class; twin-worker triggers delivery when required version differs.

# Cloud → device (OTA command)
to/organization/{orgId}/device/{deviceId}/firmware/update

# Device → cloud (progress / result)
from/organization/{orgId}/device/{deviceId}/firmware/status

Firmware status payload (conceptual)

Exact fields depend on firmware; UI shows last reported slot and version.

{
  "current_version": "1.2.0",
  "target_version": "1.3.0",
  "phase": "downloading",
  "progress_percent": 45
}

Members & roles

What it is for

Team access inside an organization: who can manage classes, devices, tokens, and org settings.

How it works

Roles include owner, admin, developer, and member with different permissions. Invites add existing users or create new accounts bound to the org.

Workflow

  1. Open Organizations → pick your org → Members.
  2. Invite by email or create a user with login and password.
  3. Assign owner, admin, developer, or member role.
  4. Members switch org in the header to collaborate on the same fleet.

Examples

Invite member (API body)

POST /v1/organizations/{orgId}/members

{
  "email": "[email protected]",
  "role": "admin"
}