Command Scripts

Lists of commands can be entered into a script file and stored in the VFS for execution (in the /store/scripts directory). These are called ‘command scripts’ and are simple sequential lists of OVMS commands. A command script can be executed with:

OVMS# . <script>
OVMS# script run <script>

Command scripts can also be stored in the /store/events/<eventname> directory structure. Whenever events are triggered, all the scripts in the corresponding /store/events/<eventname> directory are executed. Event scripts are executed in alphanumerical order of their names. Good practice is to prefix script names with 2-3 digit numbers in steps of 10 or 100 (i.e. first script named 50-…), so new scripts can easily be integrated at a specific place.

Output of background scripts without console association (e.g. event scripts) will be sent to the log with tag script at “info” level.

Note that the developer building firmware can optionally set the OVMS_DEV_SDCARDSCRIPTS build flag. If that is set, then the system will also check /sd/scripts and /sd/events for scripts. This should not be used for production builds, as you could hack the system just by plugging in an SD card.

In addition to command scripts, more sophisticated scripting capabilities may be enabled if the JavaScript environment is enabled in the build. This is discussed in the next section of this guide.


OVMS v3 includes a powerful JavaScript engine. In addition to the standard, relatively fixed, firmware flashed to the module, JavaScripting can be used to dynamically load script code to run alongside the standard firmware. This javascript code can respond to system events, and perform background monitoring and other such tasks.

The simplest way of running javascript is to place a piece of javascript code in the /store/scripts directory, with the file extension .js. Then, the standard mechanism of running scripts can be employed:

OVMS# . <script.js>
OVMS# script run <script.js>

Short javascript snippets can also be directly evaluated with:

OVMS# script eval <code>

Such javascript code can also be placed in the /store/events/<eventname> directories, to be automatically executed when the specified event is triggered. The script file name suffix must be .js to run the Javascript interpreter.


The scripting engine used is Duktape. Duktape supports ECMAScript E5/E5.1 with some additions from later ECMAScript standards. Duktape does not emulate a browser environment, so you don’t have window or document objects etc., just core Javascript plus the OVMS API and plugins.

Duktape builtin objects and functions:

Persistent JavaScript

When a javascript script is executed, it is evaluated in the global javascript context. Care should be taken that local variables may pollute that context, so it is in general recommended that all JavaScript scripts are wrapped:

  … user code …

It is also possible to deliberately load functions, and other code, into the global context persistently, and have that code permanently available and running. When the JavaScript engine initialises, it automatically runs a special startup script:


That script can in turn include other code. If you make a change to such persistent code, and want to reload it, you can with:

OVMS# script reload

JavaScript Modules

The OVMS JavaScript engine supports the concept of modules (using the node.js style of exports). Such modules can be written like this:

exports.print = function(obj, ind) {
  var type = typeof obj;
  if (type == "object" && Array.isArray(obj)) type = "array";
  if (!ind) ind = '';

  switch (type) {
    case "string":
      print('"' + obj.replace(/\"/g, '\\\"') + '"');
    case "array":
      for (var i = 0; i < obj.length; i++) {
        print(ind + '  ');
        exports.print(obj[i], ind + '  ');
        if (i != obj.length-1) print(',');
      print(ind + ']');
    case "object":
      var keys = Object.keys(obj);
      for (var i = 0; i < keys.length; i++) {
        print(ind + '  "' + keys[i] + '": ');
        exports.print(obj[keys[i]], ind + '  ');
        if (i != keys.length-1) print(',');
      print(ind + '}');

  if (ind == '') print('\n');

By convention, modules such as this are placed in the /store/scripts/lib directory as <modulename>.js. These modules can be loaded with:

JSON = require("lib/JSON");

And used as:


To automatically load a custom module on startup, add the MyPlugin = require("lib/MyPlugin"); line to ovmsmain.js.

There are a number of internal modules already provided with the firmware, and by convention these are provided under the int/<modulename> namespace. The above JSON module is, for example, provided as int/JSON and automatically loaded into the global context. These internal modules can be directly used (so JSON.print(this) works directly).

Testing JavaScript / Modules

Use the editor (see Tools menu) to test or evaluate arbitrary Javascript code. This can be done on the fly, i.e. without saving the code to a file first. Think of it as a server side Javascript shell.

Testing modules normally involves reloading the engine, as the require() call caches all loaded modules until restart. To avoid this during module development, use the following template code. This mimics the require() call without caching and allows to do tests within the same evaluation run:

// Load module:
mymodule = (function(){
  exports = {};

  // … insert module code here …

  return exports;

// Module API tests:

As the module is actually loaded into the global context this way just like using require(), anything else using the module API (e.g. a web plugin) will also work after evaluation.

Heap Memory

Due to limitations of the general esp-idf system memory management, Duktape will normally use the custom memory manager umm_malloc by Ralph Hempel.

umm_malloc needs a dedicated chunk of the system memory to work with. The default for Duktape is 512 KB (taken from SPIRAM), which is normally sufficient even for extended scripting. If you need more RAM, the size can be changed by config set module duktape.heapsize <size_in_KB>. Maximum allowed size is 1024 KB. The heap size needs to be configured at Duktape startup and cannot be changed while Duktape is running. To reconfigure the heap size, set the new configuration and do a script reload.

Call meminfo() to query the current heap memory usage status. The function returns an object containing some standard and some memory library internal info. The shell command script meminfo outputs the object in JSON format. Example:

OVMS# script meminfo
  "totalBytes": 524224,
  "usedBytes": 273344,
  "freeBytes": 250880,
  "largestFreeBytes": 180608,
  "memlib": "umm",
  "ummTotalEntries": 2723,
  "ummUsedEntries": 2615,
  "ummFreeEntries": 108,
  "ummTotalBlocks": 16382,
  "ummUsedBlocks": 8542,
  "ummFreeBlocks": 7840,
  "ummMaxFreeContiguousBlocks": 5644,
  "ummUsageMetric": 108,
  "ummFragmentationMetric": 27

“largestFreeBytes” is the largest block of contiguous memory available. Note these values will change by some amount between the garbage collection runs done every 60 seconds, the maximum usage will be just before the garbage collection, and the base line just after.

“memlib” tells about the memory manager in use, the following fields are the internal state variables and statistics of that manager (having the memlib name as a name prefix). These can be useful to monitor the memory management load and performance.

If running a firmware configured to use the default system memory manager, the output will look like this:

OVMS# script meminfo
  "totalBytes": 4072176,
  "usedBytes": 415996,
  "freeBytes": 3656180,
  "largestFreeBytes": 3635864,
  "memlib": "sys",
  "sysMinimumFreeBytes": 3653072,
  "sysAllocatedBlocks": 6013,
  "sysFreeBlocks": 454,
  "sysTotalBlocks": 6467

Internal Objects and Functions/Methods

A number of OVMS objects have been exposed to the JavaScript engine, and are available for use by custom scripts via the global context.

The global context is the analog to the window object in a browser context, it can be referenced explicitly as this on the JavaScript toplevel or as globalThis from any context.

You can see the global context objects, methods, functions and modules with the JSON.print(this) method:

OVMS# script eval 'JSON.print(this)'
  "performance": {
    "now": function now() { [native code] }
  "assert": function () { [native code] },
  "print": function () { [native code] },
  "write": function () { [native code] },
  "meminfo": function () { [native code] },
  "OvmsCommand": {
    "Exec": function Exec() { [native code] }
  "OvmsConfig": {
    "Delete": function Delete() { [native code] },
    "Get": function Get() { [native code] },
    "Instances": function Instances() { [native code] },
    "Params": function Params() { [native code] },
    "Set": function Set() { [native code] }
  "OvmsEvents": {
    "Raise": function Raise() { [native code] }
  "OvmsLocation": {
    "Status": function Status() { [native code] }
  "OvmsMetrics": {
    "AsFloat": function AsFloat() { [native code] },
    "AsJSON": function AsJSON() { [native code] },
    "Value": function Value() { [native code] }
  "OvmsNotify": {
    "Raise": function Raise() { [native code] }
  "OvmsVehicle": {
    "ClimateControl": function ClimateControl() { [native code] },
    "Homelink": function Homelink() { [native code] },
    "Lock": function Lock() { [native code] },
    "SetChargeCurrent": function SetChargeCurrent() { [native code] },
    "SetChargeMode": function SetChargeMode() { [native code] },
    "SetChargeTimer": function SetChargeTimer() { [native code] },
    "StartCharge": function StartCharge() { [native code] },
    "StartCooldown": function StartCooldown() { [native code] },
    "StopCharge": function StopCharge() { [native code] },
    "StopCooldown": function StopCooldown() { [native code] },
    "Type": function Type() { [native code] },
    "Unlock": function Unlock() { [native code] },
    "Unvalet": function Unvalet() { [native code] },
    "Valet": function Valet() { [native code] },
    "Wakeup": function Wakeup() { [native code] }
  "JSON": {
    "format": function () { [ecmascript code] },
    "print": function () { [ecmascript code] }
  "PubSub": {
    "publish": function () { [ecmascript code] },
    "subscribe": function () { [ecmascript code] },
    "clearAllSubscriptions": function () { [ecmascript code] },
    "clearSubscriptions": function () { [ecmascript code] },
    "unsubscribe": function () { [ecmascript code] }

Global Context

  • assert(condition,message)

    Assert that the given condition is true. If not, raise a JavaScript exception error with the given message.

  • print(string)

    Print the given string on the current terminal. If no terminal (for example a background script) then print to the system console as an informational message.

  • write(string/Uint8Array)

    Write the given string or Uint8Array to the current output channel (i.e. terminal/HTTP connection). Use this to transfer binary data to a reader.

  • meminfo()

    Returns an object containing the current heap memory status (see Heap Memory).


    Returns monotonic time since boot in milliseconds, with microsecond resolution.


The JSON module extends the native builtin JSON.stringify and JSON.parse methods by a format and a print method, to format and/or print out a given javascript object in JSON format. Both by default insert spacing and indentation for readability and accept an optional false as a second parameter to produce a compact version for transmission.

  • JSON.print(data)

    Output data (any Javascript data) as JSON, readable

  • JSON.print(data, false)

    …compact (without spacing/indentation)

  • str = JSON.format(data)

    Format data as JSON string, readable

  • str = JSON.format(data, false)

    …compact (without spacing/indentation)

  • JSON.stringify(value[, replacer[, space]])

    see MDN JSON/stringify

  • JSON.parse(text[, reviver])

    see MDN JSON/parse


The JSON module is provided for compatibility with standard Javascript object dumps and for readability. If performance is an issue, consider using the Duktape native builtins JSON.stringify() / Duktape.enc() and JSON.parse() / Duktape.dec() (see Duktape builtins and Duktape JSON for explanations of these).

For example, Duktape.enc('jc', data) is equivalent to JSON.format(data, false) except for the representation of functions. Using the jx encoding will omit unnecessary quotings.


All Duktape JSON encoders and decoders have a very high performance penalty and should be avoided for large objects or frequent encoding/decoding, with large being any object larger than a handful of configuration or state variables.

For general data storage and exchange with the web UI, use the CBOR serialization instead.


CBOR is a binary serialization format, and especially with Duktape the better alternative over JSON for storage and data transmission, if human readability isn’t required.

“CBOR” stands for “Concise Binary Object Representation”. See for details on the specification and available implementations. CBOR isn’t necessarily more compact in storage space, but can be encoded and decoded much faster and with much less memory overhead as JSON.

Duktape implements CBOR support by the builtin CBOR.encode() and CBOR.decode() methods:

  • enc = CBOR.encode(data)

    Encode data (any Javascript data) to CBOR format (result is an ArrayBuffer)

  • data = CBOR.decode(enc)

    Decode CBOR format (ArrayBuffer/Uint8Array) to Javascript data

CBOR support in Duktape is still considered experimental, but the underlying implementation is mature.

CBOR also isn’t part of the standard browser builtin Javascript APIs yet, so the OVMS web framework includes the cbor-js library by Patrick Gansterer (same API as on the Duktape side).

The webserver command API supports binary output from commands & Javascript API methods, and the output can be passed to CBOR.decode() directly.


The following scheme shows how to transmit a javascript data object from the module backend into the web frontend:

// Module backend:
backend.getdata = function () {
  var mydata = { pi: 3.141, fib: [ 0,1,1,2,3,5,8,13 ] };

// Web frontend:
loadjs({ command: "backend.getdata()", output: "binary" }).done((stream) => {
  var mydata = CBOR.decode(stream);

For full examples, see the “AuxBatMon” and “PwrMon” plugins.


When loading CBOR data via VFS.Load(), you need to set the binary option to true, so the loader will return a Uint8Array instead of a standard string.


The HTTP API provides asynchronous GET & POST requests for HTTP and HTTPS. Requests can return text and binary data and follow 301/302 redirects automatically. Basic authentication is supported (add username & password to the URL), digest authentication is not yet implemented.

The handler automatically excludes the request objects from garbage collection until finished (success/failure), so you don’t need to store a global reference to the request.

  • req = HTTP.Request(cfg)

    Perform asynchronous HTTP/HTTPS GET or POST request.

    Pass the request parameters using the cfg object:

    • url: standard URL/URI syntax, optionally including user auth and query string

    • post: optional POST data, set to an empty string to force a POST request. Note: you need to provide this in encoded form. If no Content-Type header is given, it will default to x-www-form-urlencoded.

    • headers: optional array of objects containing key-value pairs of request headers. Note: User-Agent will be set to the standard OVMS user agent if not present here.

    • timeout: optional timeout in milliseconds, default: 120 seconds.

    • binary: optional flag: true = perform a binary request (see response object).

    • done: optional success callback function, called with the response object as argument, with this pointing to the request object.

    • fail: optional error callback function, called with the error string as argument, with this pointing to the request object.

    • always: optional final callback function, no arguments, this = request object.

    The cfg object is extended and returned by the API (req). It will remain stable at least until the request has finished and callbacks have been executed. On completion, the req object may contain an updated url and a redirectCount if redirects have been followed. Member error (also passed to the fail callback) will be set to the error description if an error occurred. The always callback if present is called in any case, after a done or fail callback has been executed. Check this.error in the always callback to know if an error occurred.

    On success, member object response will be present and contain:

    • statusCode: the numerical HTTP Status response code

    • statusText: the HTTP Status response text

    • headers: array of response headers, each represented by an object { <name>: <value> }

    • body: only for text requests: response body as a standard string

    • data: only for binary requests: response body as a Uint8Array

    Notes: any HTTP response from the server is considered success, check response.statusCode for server specific errors. Callbacks are executed without an output channel, so all print outputs will be written to the system log. Hint: use JSON.print(this, false) in the callback to get a debug log dump of the request.


    // simple POST, ignore all results:
    HTTP.Request({ url: "http://smartplug.local/switch", post: "state=on&when=now" });
    // fetch and inspect a JSON object:
      url: "http://solarcontroller.local/status?fmt=json",
      done: function(resp) {
        if (resp.statusCode == 200) {
          var status = JSON.parse(resp.body);
          if (status["power"] > 5000)
          else if (status["power"] < 3000)
    // override user agent, log completed request object:
      url: "",
      headers: [{ "User-Agent": "Mr. What Zit Tooya" }],
      always: function() { JSON.print(this, false); }
  • HTTP.request()

    Legacy alias for HTTP.Request(), please do not use.


SSL requests (https) can take up to 12 seconds on an idle module. SSL errors also may not reflect the actual error, for example an empty server response with code 400 may be reported as a general “SSL error”. If you get “SSL error” on a valid request, you may need to install a custom root CA certificate; see SSL/TLS.


The VFS API provides asynchronous loading and saving of files on /store and /sd. Text and binary data is supported. Currently only complete files can be loaded, the saver supports an append mode. In any case, the data to save/load needs to fit into RAM twice, as the buffer needs to be converted to/from Javascript.

The handler automatically excludes the request objects from garbage collection until finished (success/failure), so you don’t need to store a global reference to the request.

Loading or saving protected paths (/store/ovms_config/…) is not allowed. Saving to a path automatically creates missing directories.

See AuxBatMon: 12V History Chart for a complete application usage example.

  • req = VFS.Load(cfg)

    Perform asynchronous file load.

    Pass the request parameters using the cfg object:

    • path: full file path, e.g. /sd/mydata/telemetry.json

    • binary: optional flag: true = perform a binary request, returned data will be an Uint8Array)

    • done: optional success callback function, called with the data content read as the single argument, this pointing to the request object

    • fail: optional error callback function, called with the error string as argument, with this pointing to the request object

    • always: optional final callback function, no arguments, this = request object

    The cfg object is extended and returned by the API (req). It will remain stable at least until the request has finished and callbacks have been executed. On success, the req object contains a data property (also passed to the done callback), which is either a string (text mode) or a Uint8Array (binary mode).

    Member error (also passed to the fail callback) will be set to the error description if an error occurred. The always callback if present is called in any case, after a done or fail callback has been executed. Check this.error in the always callback to know if an error occurred.


    // Load a custom telemetry object from a JSON file on SD card:
    var telemetry;
      path: "/sd/mydata/telemetry.json",
      done: function(data) {
        telemetry = Duktape.dec('jx', data);
        // …process telemetry…
      fail: function(error) {
        print("Error loading telemetry: " + error);
  • req = VFS.Save(cfg)

    Perform asynchronous file save.

    Pass the request parameters using the cfg object:

    • data: the string or Uint8Array to save

    • path: full file path (missing directories will automatically be created)

    • append: optional flag: true = append to the end of the file (also creating the file as necessary)

    • done: optional success callback function, called with no arguments, this pointing to the request object

    • fail: optional error callback function, called with the error string as argument, with this pointing to the request object

    • always: optional final callback function, no arguments, this = request object

    The cfg object is extended and returned by the API (req). It will remain stable at least until the request has finished and callbacks have been executed.

    Member error (also passed to the fail callback) will be set to the error description if an error occurred. The always callback if present is called in any case, after a done or fail callback has been executed. Check this.error in the always callback to know if an error occurred.


    // Save the above telemetry object in JSON format on SD card:
      path: "/sd/mydata/telemetry.json",
      data: Duktape.enc('jx', telemetry),
      fail: function(error) {
        print("Error saving telemetry: " + error);


File I/O, especially saving, can cause short freezes of the module!

Minimize save frequency and, if possible, avoid saving while the vehicle is in operation (driving / charging), by using a check like:

// Saving to VFS may cause short blockings, so only allow when vehicle is off:
function allowSave() {
  return !OvmsMetrics.Value("v.e.on") && !OvmsMetrics.Value("v.c.charging");


Saving to and loading from SD card:

When storing plugin data on an SD card, the plugin needs to take care of the SD card being mounted later in the boot process than the scripts are loaded. Plugins additionally may need to take into account, that the user may replace the SD card any time.

When trying to save or load from an unmounted SD, error will be set to volume not mounted. If this happens during plugin initialization, the plugin should subscribe to the SD mount event to retry the load/save as soon as the SD card becomes available.

Code scheme:

var storeFile = "/sd/usr/history.cbor";
var listen_sdmount = null;
var history = {};

function loadStoreFile() {
    path: storeFile,
    binary: true,
    done: function(data) {
      print(storeFile + " loaded\n");
      history = CBOR.decode(data);
    fail: function(error) {
      print(storeFile + ": " + this.error + "\n");
      if (!listen_sdmount && this.error == "volume not mounted") {
        // retry once after SD mount:
        listen_sdmount = PubSub.subscribe("sd.mounted", loadStoreFile);
      } else {

function startRecording() {
  if (listen_sdmount) {
    listen_sdmount = null;
  PubSub.subscribe(tickerEvent, ticker); // for example

if (storeFile) {
} else {


The PubSub module provides access to a Publish-Subscribe framework. In particular, this framework is used to deliver events to the persistent JavaScript framework in a high performance flexible manner. An example script to print out the ticker.10 event is:

var myTicker=function(msg,data){ print("Event: "+msg+"\n"); };


The above example created a function myTicker in global context, to print out the provided event name. Then, the PubSub.subscribe module method is used to subscribe to the ticker.10 event and have it call myTicker every ten seconds. The result is “Event: ticker.10” printed once every ten seconds.

PubSub interprets events similar to MQTT as hierarchical topics, with dots separating the levels. It delivers the events in multiple passes, with each new pass removing the last dotted part of the topic (i.e. bottom-up), so the most specific subscriptions will be called first. The handler is always called with the original event/topic name. So to e.g. catch all events vehicle.charge.…, you can simply subscribe to vehicle.charge and inspect the actual event name in your handler:

PubSub.subscribe("vehicle.charge", function (event) {
  print("Got charging related event: " + event);
  • id = PubSub.subscribe(topic, handler)

    Subscribe the function handler to messages of the given topic. Note that types are not limited to OVMS events. The method returns an id to be used to unsubscribe the handler.

  • PubSub.publish(topic, [data])

    Publish a message of the given topic. All subscribed handlers will be called with the topic and data as arguments. data can be any Javascript data.

  • PubSub.unsubscribe(id | handler | topic)

    Cancel a specific subscription, all subscriptions of a specific handler or all subscriptions to a topic.


  • str = OvmsCommand.Exec(command)

    The OvmsCommand object exposes one method “Exec”. This method is passed a single parameter as the command to be executed, runs that command, and then returns the textual output of the command as a string. For example:

    print(OvmsCommand.Exec("boot status"));
    Last boot was 14 second(s) ago
      This is reset #0 since last power cycle
      Detected boot reason: PowerOn (1/14)
      Crash counters: 0 total, 0 early


  • array = OvmsConfig.Params()

    Returns the list of available configuration parameters.

  • array = OvmsConfig.Instances(param)

    Returns the list of instances for a specific parameter.

  • string = OvmsConfig.Get(param,instance,default)

    Returns the specified parameter/instance value.

  • object = OvmsConfig.GetValues(param, [prefix])

    Gets all param instances matching the optional prefix with their associated values. If a prefix is given, the returned property names will have the prefix removed. Note: all values are returned as strings, you need to convert them as needed.

  • OvmsConfig.Set(param,instance,value)

    Sets the specified parameter/instance value.

  • OvmsConfig.SetValues(param, prefix, object)

    Sets all properties of the given object as param instances after adding the prefix. Note: non-string property values will be converted to their string representation.

  • OvmsConfig.Delete(param,instance)

    Deletes the specified parameter instance.

Beginning with firmware release 3.2.009, a dedicated configuration parameter usr is provided for plugins. You can add new config instances simply by setting them, for example by OvmsConfig.Set("usr", "myplugin.level", 123) or by the config set command.

Read plugin configuration example:

// Set default configuration:
var cfg = { level: 100, enabled: "no" };

// Read user configuration:
Object.assign(cfg, OvmsConfig.GetValues("usr", "myplugin."));

if (cfg["enabled"] == "yes") {
  print("I'm enabled at level " + Number(cfg["level"]));

Keep in mind to prefix all newly introduced instances by a unique plugin name, so your plugin can nicely coexist with others.


This provides access to the OVMS event system. While you may raise system events, the primary use is to raise custom events. Sending custom events is a lightweight method to inform the web UI (or other plugins) about simple state changes. Use the prefix usr. on custom event names to prevent conflicts with later framework additions.

Another use is the emulation of the setTimeout() and setInterval() browser methods by subscribing to a delayed event. Pattern:

function myTimeoutHandler() {
  // raise the timeout event again here to emulate setInterval()
PubSub.subscribe('usr.myplugin.timeout', myTimeoutHandler);

// start timeout:
OvmsEvents.Raise('usr.myplugin.timeout', 1500);
  • OvmsEvents.Raise(event, [delay_ms])

    Signal the event, optionally with a delay (milliseconds, must be given as a number). Delays are handled by the event system, the method call returns immediately.


  • isatlocation = OvmsLocation.Status(location)

    Check if the vehicle is currently in a location’s geofence (pass the location name as defined). Returns true or false, or undefined if the location name passed is not valid.

Note: to get the actual GPS coordinates, simply read metrics v.p.latitude, v.p.longitude and v.p.altitude.


  • bool = OvmsMetrics.HasValue(metricname)

    Returns whether the specified metric has a defined value. Returns undefined if metric is un-registered.

  • str = OvmsMetrics.Value(metricname [,unitcode] [,decode])

    Returns the typed value (default) or string representation (with decode = false) of the metric value optionally converted to the specified unit. Invalid unitcode or metricname will return invalid. Mismatched unitcode will be ignored.

  • num = OvmsMetrics.AsFloat(metricname [,unitcode])

    Returns the float representation of the metric value, optionally converted to the supplied unit. Un-registered metricname or invalid unitcode will return invalid. Mismatched unitcode will be ignored.

  • str = OvmsMetrics.AsJSON(metricname)

    Returns the JSON representation of the metric value.

  • obj = OvmsMetrics.GetValues([filter] [,unitcode] [,decode])

    Returns an object of all metrics matching the optional name filter/template (see below), by default decoded into Javascript types (i.e. numerical values will be JS numbers, arrays will be JS arrays etc.). The object returned is a snapshot, the values won’t be updated.

    The filter argument may be a string (for substring matching as with metrics list), an array of full metric names, or an object of which the property names are used as the metric names to get. The object won’t be changed by the call, see Object.assign() for a simple way to merge objects. Passing an object is especially convenient if you already have an object to collect metrics data.

    The decode argument defaults to true, pass false to retrieve the metrics string representations instead of typed values.

    The unitcode argument allows units to be converted (amongst the same types of untits). The special unit codes “native”, “metric” and “imperial” can also be used. Specifying an invalid unitcode will return invalid. Mismatched unitcode will be ignored on those metricnames that don’t match.

    For OvmsMetrics.Value and OvmsMetrics.GetValues if a unitcode is specified in addition to passing false to the decode argument, then the metric is returned as a string with any unit specifiers.

// Get the speed as a string with units ( eg: 37.4km/h )
var speed  = OvmsMetrics.Value("v.b.range.speed", "native", false)

With the introduction of the OvmsMetrics.GetValues() call, you can get multiple metrics at once and let the system decode them for you. Using this you can for example do:

// Get all metrics matching substring "v.b.c." (vehicle battery cell):
var metrics = OvmsMetrics.GetValues("v.b.c.");
print("Temperature of cell 3: " + metrics["v.b.c.temp"][2] + " °C\n");
print("Voltage of cell 7: " + metrics["v.b.c.voltage"][6] + " V\n");

// Get some specific metrics:
var ovmsinfo = OvmsMetrics.GetValues(["m.version", "m.hardware"]);

This obsoletes the old pattern of parsing a metric’s JSON representation using eval(), JSON.parse() or Duktape.dec() you may still find in some plugins. Example:

var celltemps = eval(OvmsMetrics.AsJSON("v.b.c.temp"));
print("Temperature of cell 3: " + celltemps[2] + " °C\n");


Never use eval() on unsafe data, e.g. user input! eval() executes arbitrary Javascript, so can be exploited for code injection attacks.


  • id = OvmsNotify.Raise(type, subtype, message)

    Send a notification of the given type and subtype with message as contents. Returns the message id allocated or 0 in case of failure. Examples:

    // send an info notification to the user:
    OvmsNotify.Raise("info", "usr.myplugin.status", "Alive and kicking!");
    // send a JSON stream to a web plugin:
    OvmsNotify.Raise("stream", "usr.myplugin.update", JSON.format(streamdata, false));
    // send a CSV data record to a server:
    OvmsNotify.Raise("data", "usr.myplugin.record", "*-MyStatus,0,86400,Alive");


The OvmsVehicle object is the most comprehensive, and exposes several methods to access the current vehicle. These include:

  • str = OvmsVehicle.Type()

    Return the type of the currently loaded vehicle module

  • success = OvmsVehicle.Wakeup()

    Wakeup the vehicle (return TRUE if successful)

  • success = OvmsVehicle.Homelink(button, durationms)

    Fire the given homelink button

  • success = OvmsVehicle.ClimateControl(onoff)

    Turn on/off climate control

  • success = OvmsVehicle.Lock(pin)

    Lock the vehicle

  • success = OvmsVehicle.Unlock(pin)

    Unlock the vehicle

  • success = OvmsVehicle.Valet(pin)

    Activate valet mode

  • success = OvmsVehicle.Unvalet(pin)

    Deactivate valet mode

  • success = OvmsVehicle.SetChargeMode(mode)

    Set the charge mode (“standard” / “storage” / “range” / “performance”)

  • success = OvmsVehicle.SetChargeCurrent(limit)

    Set the charge current limit (in amps)

  • success = OvmsVehicle.SetChargeTimer(onoff, start)

    Set the charge timer

  • success = OvmsVehicle.StartCharge()

    Start the charge

  • success = OvmsVehicle.StopCharge()

    Stop the charge

  • success = OvmsVehicle.StartCooldown()

    Start a cooldown charge

  • success = OvmsVehicle.StopCooldown()

    Stop the cooldown charge

  • result = OvmsVehicle.ObdRequest(arguments)

    Perform OBD/UDS request (synchronous)

    Pass the request parameters using the arguments object:

    • txid: the CAN ID to send the request to (or 0x7df for broadcast)

    • rxid: the CAN ID to expect the response at (or 0 for broadcast)

    • request: the request to send, either a hex encoded string or an Uint8Array

    • bus: optional CAN bus device name, default “can1”

    • timeout: optional timeout in milliseconds, default 3000

    • protocol: optional protocol to use, default 0 = ISOTP_STD – see vehicle.h for other protocols

    The result object will have these properties:

    • error: 0 = no error, else the error code, with negative ranges being system errors, positive codes are OBD/UDS response error codes (NRCs)

    • errordesc: a human readable error description

    • response: only on success: the binary response (Uint8Array)

    • response_hex: only on success: hex encoded response (string)


    // Establish diagnostic session with an ECU:
    var res = OvmsVehicle.ObdRequest({ txid: 0x765, rxid: 0x7cf, request: "1003" });
    if (res.error)

OvmsVehicle Command Plugins

Most vehicles do not implement all standard vehicle commands. When trying to execute one of these, the system will respond with “not implemented”. Using the OvmsVehicle object, you can register your own handlers for these. This applies to the following functions:

  • OvmsVehicle.Wakeup()

  • OvmsVehicle.Homelink(button, durationms)

  • OvmsVehicle.ClimateControl(onoff)

  • OvmsVehicle.Lock(pin)

  • OvmsVehicle.Unlock(pin)

  • OvmsVehicle.Valet(pin)

  • OvmsVehicle.Unvalet(pin)

  • OvmsVehicle.SetChargeMode(mode)

  • OvmsVehicle.SetChargeCurrent(limit)

  • OvmsVehicle.SetChargeTimer(onoff, start)

  • OvmsVehicle.StartCharge()

  • OvmsVehicle.StopCharge()

  • OvmsVehicle.StartCooldown()

  • OvmsVehicle.StopCooldown()

Note: this normally only works for commands not implemented by the vehicle. Vehicles may also allow custom handlers to replace their default implementation, ask a vehicle maintainer if you miss/need this option for your vehicle.

To register your own command handler for any of these, simply assign a Javascript function to the respective OvmsVehicle property. Your custom function shall accept the same arguments as the native handler and return a boolean value to reflect success (true) or failure (false).


A common use case for this is implementing your own variant of “Homelink”, which is basically only available in hardware on Tesla Roadsters, yet accessible in the App for other vehicles as well as a means to execute other commands.

The following example code shows how to register a custom “Homelink” handler doing an HTTP API call:

OvmsVehicle.Homelink = function(button, durationms) {
  const debug = false;   // set to true to log full server response
  const notify = false;  // set to true to enable failure push notification
  const fndesc = "Homelink " + button;

    url: "https://your.api.server/action?button=" + button,
    done: function() { print(fndesc + " OK"); },
    fail: function() { const msg = fndesc + " FAILED: " + this.error; print(msg);
      if (notify) OvmsNotify.Raise("alert", "homelink", msg); },
    always: function() { if (debug) print(JSON.stringify(this.response||this)); }

  return true;

(Due to the HTTP request being asynchronous, the command function can only return true when called. Enable the push notification to get an alert on failure.)

After running this code for a vehicle not implementing the homelink command itself, you can let the module do the HTTP API call by selecting one of the three button options in the App, as well as by executing the homelink shell command.

To load this plugin automatically on boot, add the code to ovmsmain.js, either inline or by loading a lib module (see Persistent JavaScript).

Test Utilities

You can use the web UI editor and shell to edit, upload and test script files. If you need many test cycles, a convenient alternative is to use shell scripts to automate the process.

If you’ve configured ssh public key authentication, you can simply use scp to upload scripts and ssh to execute commands:

# Upload & execute a script file:



SCP="/usr/bin/scp -q"

# Upload:

# Execute:
$SSH "${OVMS_HOST}" "script run ${FILE}"

Customize to your needs. If you want to test a plugin, simply replace the script run command by script reload followed by some script eval calls to your plugin API.

Note: this may be slow, as the ssh session needs to be negotiated for every command.


With OpenSSH version 9.0 (or later), the scp protocol has been disabled by default and replaced by the sftp protocol. To be able to use the scp command with OVMS, you need to re-enable the scp protocol with option -O on the command line:

scp -O ....

A faster option is using the OVMS HTTP REST API. The following script uses curl to upload and execute a script:

# Upload & execute a script file:



CURL="/usr/bin/curl -c .auth -b .auth"

# Login?
if [[ -e ".auth" ]] ; then
  AUTHAGE=$(($($DATE +%s) - $($DATE +%s -r ".auth")))
if [[ "$AUTHAGE" -ge 3600 ]] ; then
  RES=$($CURL "${OVMS_HOST}/login" --data-urlencode "username=${OVMS_USER}" --data-urlencode "password=${OVMS_PASS}" 2>/dev/null)
  if [[ "$RES" =~ "Error" ]] ; then
    echo -n "LOGIN ERROR: "
    echo $RES | $SED -e 's:.*<li>\([^<]*\).*:\1:g'
    rm .auth
    exit 1

# Upload:
RES=$($CURL "${OVMS_HOST}/edit" --data-urlencode "path=${PATH}${FILE}" --data-urlencode "content@${FILE}" 2>/dev/null)
if [[ "$RES" =~ "Error" ]] ; then
  echo -n "UPLOAD ERROR: "
  echo $RES | $SED -e 's:.*<li>\([^<]*\).*:\1:g'
  rm .auth
  exit 1

# Execute:
$CURL "${OVMS_HOST}/api/execute" --data-urlencode "command=script run ${FILE}"