The device model is one of OCPP 2.0.1’s biggest architectural improvements over 1.6. It replaces 1.6’s flat namespace of “configuration keys” with a structured, hierarchical representation of what a charger is. Components, variables, operational status, monitoring data — all organized into a tree that mirrors the physical and logical reality of a charger. If you need a refresher on the protocol itself first, start with what OCPP is and the version comparison (coming soon).
This article explains what the device model is, why it exists, and how to think about it as either a CSMS implementer or a charge point firmware developer.
What problem the device model solves
In OCPP 1.6, the configuration interface looked like this:
GetConfiguration → returns:
HeartbeatInterval = 300
MeterValueSampleInterval = 60
MeterValuesSampledData = "Energy.Active.Import.Register,Power.Active.Import"
AllowOfflineTxForUnknownId = true
AuthorizeRemoteTxRequests = false
... (a flat list of name=value pairs)
This worked but had problems.
Names ran out. As OCPP grew to cover more capabilities, names got longer and more contextual (“MaxChargingProfilesInstalled” — installed where? at the charger level? at the connector level?).
No structure. “What does this charger actually look like?” required asking many questions and stitching answers together.
Hard to extend. New configuration items meant new names, which meant CSMS code that had to know about each individual name.
Per-connector configuration was awkward. “Set the max amps on connector 2” required knowing the right name convention.
The device model addresses all of these by giving the charger a proper internal structure with components and variables.
The basic structure
In OCPP 2.0.1, a charger is a tree:
ChargingStation (root)
├── EVSE 1
│ ├── Connector 1
│ ├── Connector 2 (if applicable)
│ └── MeterReader
├── EVSE 2 (if multi-EVSE)
│ └── Connector 1
├── SecurityController
├── DisplayController
├── AlignedDataController
├── SmartChargingController
├── LocalAuthListController
└── ... (more)
Viewed as a hierarchy, the same tree looks like this:
flowchart TD
CS[ChargingStation root]
CS --> E1[EVSE 1]
CS --> E2[EVSE 2]
CS --> SEC[SecurityController]
CS --> DISP[DisplayController]
CS --> SC[SmartChargingController]
E1 --> C1[Connector 1]
E1 --> C2[Connector 2]
E1 --> MR[MeterReader]
E2 --> C3[Connector 1]
style CS fill:#1e3a5f,stroke:#0c2340,color:#fff
Each node is a Component. Each Component has Variables — configurable or queryable attributes.
So instead of a flat name like “MaxChargingProfilesInstalled,” you have:
Component: SmartChargingCtrlr
Variable: MaxChargingProfilesInstalled = 10
Variable: Enabled = true
Variable: ACPhaseSwitchingSupported = false
The Variable’s full identity is (Component, Variable name). This is naturally scoped and extensible.
Components and Variables
A few specific Components you’ll encounter often:
ChargingStation — the root. Holds station-level config like HeartbeatInterval, AllowOfflineTxForUnknownId, etc.
EVSE — represents one charging point (one set of connector(s) sharing a power module). A station can have multiple EVSEs. Variables include OperationalStatus, AvailabilityState, Power.
Connector — the physical socket or cable. Variables include ConnectorType (cCCS1, cCCS2, cType1, cType2, cCHAdeMO, cNACS, etc.), OperationalStatus.
SecurityController — security-related config. CertificateEntries, ChargingStationCertificate management, BasicAuthPassword for legacy, etc.
DisplayController — controls what’s shown on the charger’s display (if it has one). DisplayMessage management lives here.
AlignedDataController — controls clock-aligned data sampling (independent of transactions). MeterValueAlignedData, ClockAlignedDataInterval.
SampledDataController — controls transaction-sampled data. MeterValueSampleInterval, MeterValuesSampledData.
SmartChargingController — smart charging config. MaxChargingProfilesInstalled, ChargingScheduleMaxPeriods.
LocalAuthListController — local user-authorization whitelist config.
MonitoringController — operational monitoring config. What to monitor, what thresholds trigger alarms.
Many more exist. The full list is in the OCPP 2.0.1 spec; this is a representative sample.
Variables: characteristics and attributes
Each Variable has multiple aspects:
- Name — the variable’s name within its component.
- Type — String, Integer, Decimal, Boolean, DateTime, MemberList, OptionList, SequenceList.
- Mutability — ReadOnly, ReadWrite, or WriteOnly.
- Persistency — does the value survive reboot?
- Unit — for numerical types.
- MinLimit / MaxLimit — bounds.
- Values — allowed enumeration values.
- InstanceId — to support multiple instances of the same variable (e.g. multiple thresholds for monitoring).
This is structured metadata about the configuration item itself, not just the value. The CSMS can discover not just what variables exist but what values are valid.
The relevant messages
OCPP 2.0.1 has several messages built around the device model.
GetVariables — read variable values. Can request many variables in one call.
SetVariables — set variable values. Can set many in one call.
GetVariableMonitoring — query the monitoring configuration of variables.
SetVariableMonitoring — configure monitoring (alarm me when this variable crosses this threshold).
GetReport — request a structured report (similar to GetVariables but with more flexibility on what to include).
NotifyReport — push a structured report from charger to CSMS. Used for reporting that includes the full device model structure when needed.
ReportChargingProfiles — list active charging profiles in a structured way.
The pattern: the CSMS works at the level of components and variables; the charger maintains the device model and answers structured queries. This same structured-message philosophy shows up elsewhere in 2.0.1 — see how the TransactionEvent message (coming soon) reshapes transaction reporting.
How discovery works
A CSMS wanting to fully understand a charger:
- GetBaseReport (or similar) — ask the charger for its full device model.
- Charger responds via NotifyReport with the complete tree of components and variables.
- CSMS now knows which components this charger has, which variables exist, which values are allowed, etc.
This is the device model’s discovery story. Compared to 1.6’s “GetConfiguration returns a flat list of well-known names,” the 2.0.1 version is dramatically richer.
The discovery report can be large for a complex charger — hundreds of variables across dozens of components. CSMSes typically cache it and re-query only when the device model is expected to have changed (firmware update, configuration change, etc.).
A typical configuration flow
Setting up a freshly-deployed OCPP 2.0.1 charger:
- BootNotification completes; charger registered.
- CSMS sends GetBaseReport to discover the charger’s device model.
- Charger responds with the full report via NotifyReport.
- CSMS pushes configuration via SetVariables — heartbeat interval, sampled-data interval, security profile, local-auth-list config, etc.
- CSMS reads back via GetVariables to confirm the configuration took effect.
- CSMS sets up monitoring via SetVariableMonitoring for critical variables.
- Charger now operating normally.
This is a more structured process than 1.6 where you’d just ChangeConfiguration on known names. The trade-off is more rigor up front; the benefit is that everything you’ve configured is queryable and verifiable.
Migration from 1.6
If you’re migrating a CSMS from 1.6 to 2.0.1, the device model is one of the bigger structural changes. The version comparison (coming soon) covers where else the two versions diverge.
Old config layer: key-value pairs by string name.
New config layer: structured Components → Variables with rich metadata.
Code migration:
- Map your existing 1.6 config keys to 2.0.1 (Component, Variable) pairs.
- Build a per-version configuration API in your CSMS.
- Use feature detection: ask the charger which device model version it supports.
- Support both 1.6 (ChangeConfiguration) and 2.0.1 (SetVariables) in parallel for years.
A common pattern: build an abstract config layer in your CSMS that hides the 1.6/2.0.1 distinction from your application code. Below the abstraction, route to the right message based on charger version.
Vendor extensions
The device model supports vendor-specific extensions. A vendor can define additional components and variables under their vendor namespace. The CSMS encounters them, can read/write them if it knows about them, and ignores them otherwise.
This is a clean extension mechanism that 1.6 didn’t really have. Vendor-specific behavior gets a place; it doesn’t pollute the standard namespace.
For CSMS implementers, the practical implication: support read-but-don’t-fail on unknown components and variables. Don’t fail to parse a NotifyReport because it contains a vendor-extension component you don’t recognize.
Monitoring and alarms
The device model integrates with the monitoring system. You can configure: “Alert me when EVSE.OperationalStatus on EVSE 1 changes to Faulted” or “Alert me when ChargingStation.Temperature exceeds 50 degrees.”
The charger emits NotifyEvent or NotifyMonitoringReport when configured conditions occur.
This is operationally valuable — you get charger-side monitoring that’s tied to the structured model rather than building everything from raw status notifications and meter readings.
Common pitfalls
A few things that bite teams implementing the device model.
Trying to use only well-known variables. The OCPP spec lists “standardized variables” but chargers can have vendor-specific ones too. Don’t assume your map of variable names is exhaustive.
Caching the device model too aggressively. Variables can change (especially OperationalStatus, AvailabilityState, etc.). Cache static metadata, re-query dynamic state.
Treating SetVariables as synchronous. Some variable updates take effect immediately; some require a reset; some are queued until the next opportunity. Read the spec on persistency and effective-time.
Underestimating the device model report size. A complex charger can have hundreds of variables. Plan storage and transmission accordingly.
Not supporting partial reports. A NotifyReport can be paginated across multiple messages for large device models. Handle the multi-message case.
What this means in practice
For most CSMS engineers, the device model represents real complexity that has to be implemented but, once implemented, makes the configuration story much cleaner. The investment is upfront; the payoff is over years of operations.
For charger firmware engineers, the device model means representing your charger as a proper tree of components and exposing it correctly via the OCPP messages. Most modern firmware (post-2020) has converged on similar patterns; the spec is clear enough that interop is reasonable.
The honest summary
The OCPP 2.0.1 device model is one of the cleanest improvements over 1.6. The investment in implementing it pays back in a configuration layer that’s structured, queryable, extensible, and self-describing. The migration from 1.6’s flat namespace is meaningful work but enables a more durable operational model. If you’re building OCPP code in 2026, lean into the device model rather than trying to ignore it.