PHP2Shell alert: critical PHP flaws CVE-2026-109618 / 109619 — check your servers and get patched fast
HashTech Systems
EV Charging
Part 2 of 11

OCPP 2.0.1 Device Model: Components, Variables and Monitoring

How the OCPP 2.0.1 device model works: ComponentType, VariableType, attributes, GetVariables/SetVariables, NotifyReport and variable monitoring.

HashTech Engineering · October 26, 2026 · 6 min read

In OCPP 1.6, configuration is a flat list of string keys like HeartbeatInterval and MeterValueSampleInterval, plus whatever vendor keys the manufacturer invented. OCPP 2.0.1 replaces this with the device model: a structured, self-describing view of the station as components that have variables, where each variable has typed attributes and characteristics. It is the part of 2.0.1 that takes longest to get right, and almost everything else (transactions, smart charging, security) is configured through it.

Components and variables

A component is a logical or physical part of the station: a controller like OCPPCommCtrlr or TxCtrlr, or a physical thing like a connector, a power converter or a fan. A component is identified by a ComponentType: a name, an optional instance (when there are several of the same thing), and an optional evse (to place it on an EVSE or connector).

A variable is a property of that component, identified by VariableType: a name and an optional instance. The pair (component, variable) is the address of a configuration item or a piece of state.

json
{
  "component": {
    "name": "OCPPCommCtrlr"
  },
  "variable": {
    "name": "HeartbeatInterval"
  }
}

{
  "component": {
    "name": "Connector",
    "evse": {
      "id": 1,
      "connectorId": 1
    }
  },
  "variable": {
    "name": "AvailabilityState"
  }
}

Attributes

Each variable can have up to four attributes, identified by AttributeEnumType. Most configuration variables only have Actual; the others matter for things like setpoints and limits.

AttributeEnumTypeMeaning
ActualThe current value. This is the default when attributeType is omitted.
TargetA desired value the component is working towards (for example a target temperature).
MinSetThe configured lower limit.
MaxSetThe configured upper limit.

Each attribute also carries metadata: mutability (ReadOnly, WriteOnly, ReadWrite), persistent (survives reboot) and constant (never changes). Separately, VariableCharacteristicsType describes the variable itself: dataType (string, decimal, integer, dateTime, boolean, OptionList, SequenceList, MemberList), unit, minLimit, maxLimit, valuesList for list types, and supportsMonitoring.

Standardised components you will use daily

The device model appendix of the spec defines standard component and variable names. Stations may add their own, but the standard ones are what a CSMS can rely on across vendors. A non-exhaustive list of the ones most CSMS code touches:

ComponentExample variables
OCPPCommCtrlrHeartbeatInterval, NetworkConfigurationPriority, OfflineThreshold, MessageAttempts, MessageAttemptInterval, WebSocketPingInterval
SecurityCtrlrSecurityProfile, Identity, BasicAuthPassword, OrganizationName
TxCtrlrTxStartPoint, TxStopPoint, EVConnectionTimeOut, StopTxOnEVSideDisconnect, StopTxOnInvalidId
AuthCtrlrAuthorizeRemoteStart, LocalAuthorizeOffline, LocalPreAuthorize, OfflineTxForUnknownIdEnabled
AuthCacheCtrlr / LocalAuthListCtrlrEnabled, LifeTime, Entries, ItemsPerMessage
SampledDataCtrlrTxStartedMeasurands, TxUpdatedMeasurands, TxUpdatedInterval, TxEndedMeasurands, TxEndedInterval
AlignedDataCtrlrMeasurands, Interval, TxEndedMeasurands, TxEndedInterval
SmartChargingCtrlrEnabled, ProfileStackLevel, PeriodsPerSchedule, RateUnit
DeviceDataCtrlrItemsPerMessage, BytesPerMessage (instanced per message type)

GetVariables and SetVariables

Both messages are batched: one request carries a list of component/variable pairs, and the response returns one result per entry, each with its own status. Here the CSMS reads the heartbeat interval and the transaction start point:

json
[2, "5f0c7a1e-0b1c-4c39-8a55-2b7d1f3e9c01", "GetVariables", {
  "getVariableData": [
    {
      "component": { "name": "OCPPCommCtrlr" },
      "variable": { "name": "HeartbeatInterval" }
    },
    {
      "component": { "name": "TxCtrlr" },
      "variable": { "name": "TxStartPoint" },
      "attributeType": "Actual"
    }
  ]
}]

[3, "5f0c7a1e-0b1c-4c39-8a55-2b7d1f3e9c01", {
  "getVariableResult": [
    {
      "attributeStatus": "Accepted",
      "attributeType": "Actual",
      "attributeValue": "300",
      "component": { "name": "OCPPCommCtrlr" },
      "variable": { "name": "HeartbeatInterval" }
    },
    {
      "attributeStatus": "Accepted",
      "attributeType": "Actual",
      "attributeValue": "PowerPathClosed",
      "component": { "name": "TxCtrlr" },
      "variable": { "name": "TxStartPoint" }
    }
  ]
}]

attributeStatus (GetVariableStatusEnumType) is one of Accepted, Rejected, UnknownComponent, UnknownVariable or NotSupportedAttributeType. Values are always strings on the wire, even for integers and booleans, so parse according to the variable's dataType.

json
[2, "8a7e44d2-3a0b-4e0a-b9a2-6d1c0f9b7e55", "SetVariables", {
  "setVariableData": [
    {
      "attributeType": "Actual",
      "attributeValue": "60",
      "component": { "name": "SampledDataCtrlr" },
      "variable": { "name": "TxUpdatedInterval" }
    },
    {
      "attributeValue": "Energy.Active.Import.Register,Power.Active.Import",
      "component": { "name": "SampledDataCtrlr" },
      "variable": { "name": "TxUpdatedMeasurands" }
    }
  ]
}]

[3, "8a7e44d2-3a0b-4e0a-b9a2-6d1c0f9b7e55", {
  "setVariableResult": [
    {
      "attributeType": "Actual",
      "attributeStatus": "Accepted",
      "component": { "name": "SampledDataCtrlr" },
      "variable": { "name": "TxUpdatedInterval" }
    },
    {
      "attributeType": "Actual",
      "attributeStatus": "Accepted",
      "component": { "name": "SampledDataCtrlr" },
      "variable": { "name": "TxUpdatedMeasurands" }
    }
  ]
}]

SetVariableStatusEnumType adds RebootRequired to the Get statuses: the value was stored but only takes effect after a reset. Treat it as success-with-follow-up, and track it per station so an operator can schedule the Reset rather than having the change silently not apply.

Reports: GetBaseReport, GetReport and NotifyReport

To discover what a station has, the CSMS asks for a report. GetBaseReport takes a requestId and a reportBase of ConfigurationInventory (configurable variables), FullInventory (everything) or SummaryInventory (a short status overview). The station answers the request immediately with a status, then sends the data asynchronously in one or more NotifyReport messages.

json
[2, "c0ffee01-1111-4a2b-9c3d-000000000001", "GetBaseReport", {
  "requestId": 1017,
  "reportBase": "FullInventory"
}]

[3, "c0ffee01-1111-4a2b-9c3d-000000000001", {
  "status": "Accepted"
}]
json
[2, "3e1d9b9c-77aa-4f0e-8d3a-5b2c6e7f8a90", "NotifyReport", {
  "requestId": 1017,
  "generatedAt": "2026-10-26T09:20:11.000Z",
  "seqNo": 0,
  "tbc": true,
  "reportData": [
    {
      "component": { "name": "OCPPCommCtrlr" },
      "variable": { "name": "HeartbeatInterval" },
      "variableAttribute": [
        {
          "type": "Actual",
          "value": "300",
          "mutability": "ReadWrite",
          "persistent": true,
          "constant": false
        }
      ],
      "variableCharacteristics": {
        "unit": "s",
        "dataType": "integer",
        "supportsMonitoring": false
      }
    }
  ]
}]

seqNo starts at 0 and increments per NotifyReport for the same requestId; tbc ("to be continued") is true on every part except the last. A full inventory from a DC charger can run to many parts and thousands of entries. GetReport is the filtered version: you pass componentVariable entries and/or componentCriteria (Active, Available, Enabled, Problem) to get only part of the model. The response status for both is GenericDeviceModelStatusEnumType: Accepted, Rejected, NotSupported or EmptyResultSet.

Monitoring: NotifyEvent instead of error codes

2.0.1 lets the CSMS ask the station to watch variables and report when something happens. Each monitor is set with SetVariableMonitoring and has a type from MonitorEnumType: UpperThreshold, LowerThreshold, Delta, Periodic or PeriodicClockAligned, a value, and a severity from 0 (Danger) to 9 (Debug).

json
[2, "a1b2c3d4-0000-4000-8000-00000000a001", "SetVariableMonitoring", {
  "setMonitoringData": [
    {
      "value": 85,
      "type": "UpperThreshold",
      "severity": 4,
      "component": { "name": "PowerConverter", "evse": { "id": 1 } },
      "variable": { "name": "Temperature" }
    }
  ]
}]

When a monitor fires, the station sends NotifyEvent with one or more eventData entries. Each entry has an eventId, timestamp, trigger (Alerting, Delta, Periodic), the actualValue, the component and variable, an eventNotificationType (HardWiredNotification, HardWiredMonitor, PreconfiguredMonitor, CustomMonitor), and optionally cleared, techCode and techInfo.

json
[2, "e9d8c7b6-1234-4abc-9def-0123456789ab", "NotifyEvent", {
  "generatedAt": "2026-10-26T13:02:45.000Z",
  "seqNo": 0,
  "eventData": [
    {
      "eventId": 5531,
      "timestamp": "2026-10-26T13:02:44.000Z",
      "trigger": "Alerting",
      "actualValue": "86.5",
      "techCode": "PC_OVERTEMP",
      "eventNotificationType": "CustomMonitor",
      "variableMonitoringId": 12,
      "component": { "name": "PowerConverter", "evse": { "id": 1 } },
      "variable": { "name": "Temperature" }
    }
  ]
}]

Other monitoring messages: SetMonitoringBase (All, FactoryDefault, HardWiredOnly) to choose which built-in monitors are active, SetMonitoringLevel to filter by severity, ClearVariableMonitoring, and GetMonitoringReport/NotifyMonitoringReport to list what is configured. Hardwired and preconfigured monitors from the manufacturer are where most fault reporting comes from in practice; this is where 1.6's errorCode and vendorErrorCode information now lives.

Field gotchas

  • Treat names as case-sensitive. HeartBeatInterval is not HeartbeatInterval; you will get UnknownVariable.
  • Do not assume every standard variable exists. Many are optional. Discover with a report, cache the result, and fall back gracefully.
  • Instances matter. MessageAttempts and MessageAttemptInterval on OCPPCommCtrlr use the instance TransactionEvent; omitting the instance addresses a different (usually non-existent) variable.
  • Read-only means read-only. Setting a ReadOnly attribute returns Rejected. Show mutability in your admin UI so operators stop trying.
  • Report volume. A FullInventory arriving as many NotifyReport frames right after boot competes with everything else; request it once, then use targeted GetVariables.

Next

With the device model in hand, the next step is the connection lifecycle: boot, provisioning and network profiles. If you are mapping existing 1.6 keys to variables, the migration guide has a table, and the 1.6 side is covered in configuration keys.

Let's Talk

Ready to build something that lasts?

Whether it's a charging network, an AI rollout, or a core enterprise system — let's talk about what you're building.