Technical brief

How ECC connects your site

Three layers, no surprises. Architecture deep dive for integrators and CTOs evaluating the platform before recommending it internally.

Architecture

How it works

Three layers. No surprises in any of them.

01

Edge

Docker container on a site gateway

Polls Modbus TCP registers every 30 seconds (configurable). Detects value changes locally. On WAN outage, SQLite buffer; automatic flush on reconnect.

02

Cloud

Azure IoT Hub + Cosmos DB

AMQP over TLS into IoT Hub, EventHub consumer writes to Cosmos. Device Twin holds the register map — change config via API, the edge pulls the update itself.

03

API + Dashboard

FastAPI + React

REST API with JWT auth, dashboard with a drag-and-drop builder per locality. Multi-tenant — platform admin sees the fleet, customer admin sees their sites only.

For engineers

Specifications

Concrete, marketing-free. What the edge does, how you integrate it, what security you get.

Edge module

  • Runtime: Python 3.10-slim, Docker, ~80 MB image
  • Host: Linux x86_64 nebo ARM64; tested on Azure IoT Edge 1.4 + 1.5
  • Protocols: Modbus TCP (FC 3/4/6/16); RTU on roadmap
  • Data types: UInt16, Int16, UInt32, Int32, Float32, String, Bitfield; big- + little-endian word order
  • Polling: per-register frequency from 1 s; batched reads where contiguous
  • Offline: SQLite buffer, 72h retention, automatic flush
  • Update: Device Twin hot-reload, <5 s to live
  • Mem footprint: ~120 MB resident at 200 registers / 30 s poll

Cloud + API

  • Stack: FastAPI 0.115, Pydantic v2, Motor, MongoDB / Cosmos DB
  • Ingest: Azure IoT Hub S1; EventHub consumer group ecc-functions
  • Auth: JWT, HttpOnly cookies, lockout after 5 failed logins
  • RBAC: 4 roles (admin/operator/analyst/viewer) + platform-admin; per-widget gating
  • Audit: SHA-256 hash-chain on every mutation; break detection
  • Multi-tenant: tenant filter on every Mongo query; tested via test_tenant_isolation.py
  • Rate limits: 5/min on /auth/login, 10/min on /commands, 30/min on /layouts
  • Availability: 99.9% target; Grafana + Discord alerting

Security posture

  • TLS: AMQP-S to IoT Hub; HTTPS-only API; HSTS
  • Secrets: Azure-managed identity for IoT Hub; no plaintext creds in repo
  • Cookies: HttpOnly + Secure + SameSite=lax; per-session lastLogout invalidation
  • Inactive users: deactivated accounts rejected at JWT verify time, not just at login
  • Inputs: Pydantic on every wire; whitelist for layout widget types
  • NIS2: audit chain + role-separation + incident-response logging in place
  • Disclosures: security.txt

Supported devices

  • Sinexcel PCS — PWS1-500KTL-P, 188 registers, FC3 read + FC6 write
  • CATL EnerOne+ BMS — 138 register subset (Techsys), 200+ full map
  • ABB REF615 — Relion 615 protection relay, 50+ registers
  • ABB SACE Emax 2 — LV breaker (reverse-word-order handled)
  • Sungrow Logger 1000B — gateway for Sungrow inverters (input registers)
  • Custom devices: register map in JSON, one day of work
Example device config (Device Twin) json
{
  "ModbusConfig": {
    "devices": {
      "PCS_1": {
        "connection": { "host": "192.168.5.101", "port": 502, "unit_id": 1 },
        "operations": [
          { "address": "32", "name": "SystemFaultStatus", "function_code": 3,
            "data_type": "UInt16", "frequency": 30000 },
          { "address": "100", "name": "ActivePower", "function_code": 3,
            "data_type": "Int32", "unit": "0.1kW", "frequency": 5000 }
        ]
      }
    }
  }
}

The boring bits that matter

Configuration as data, not as code

Every register a device exposes is described in a JSON map. Adding a new device type takes a day of work, not a sprint. The map lives in the cloud and is pushed to the edge module via Azure IoT Hub's Device Twin — there is no "release a new container" step to change a polling interval or an alarm threshold.

Per-role dashboard layouts

The dashboard is a thin renderer over a JSON layout document stored per locality. A drag-and-drop builder produces these documents; admins, operators, analysts and viewers each get a different view of the same site without a code change. Onboarding a new customer becomes a configuration change, not a sprint.

Audit chain

Every mutation — login, config change, command sent, user invited — is logged with a SHA-256 hash that includes the previous entry's hash. A scheduled job verifies the chain hourly and refuses to extend it on a break. Auditors receive a single export they can verify cryptographically. NIS2 logging obligation met in code, not in a binder.

Multi-tenant by construction

Every Mongo query goes through a tenant filter derived from the authenticated user's customer ID. A platform-admin (employed by EC Control) can see across customers; everyone else is hard-isolated. Cross-tenant access attempts return 404, not 403, so no information about other customers' existence is leaked.

DOC.99 — Contact REV.A

Thirty minutes, live platform.