History Sources

Manual for History Sources: the trends read the history of a variable from the EnvisionSCADA Historian, a query on a Database Connection or a Python Module script, selected per variable or as the project default.

Updated 19 min read
On this page (14 sections)
  1. Overview
  2. History Sources Page
  3. History Source Settings of a Variable
  4. Database
  5. Database on InfluxDB 3
  6. Script
  7. One fetch() Call for Several Variables
  8. Browse
  9. Example Module Script
  10. Example: Historian with a REST API
  11. Errors in Trends
  12. UDT Members and Variable Arrays
  13. Projects from an Earlier Version
  14. Notes

A History Source decides where the trends read the history of a variable. The History Sources of a project are defined on the History Sources page of the Configuration menu. Each variable selects one in the History Source section of its Historian tab, for every variable type, and so does a variable member of a UDT. The Trend Analyst, Quick Trend, the Trend object and GetHistoricData of the runtime API read the history of each variable from the History Source it selects.

A History Source applies to the history only. The live value of the variable, and the live part of an open trend, come from the variable itself. A variable that reads its history from a History Source shows that history with Historian off, so the data is not stored a second time in the EnvisionSCADA Historian.

History Source of the variableReads the history from
Project defaultThe History Source with Project default enabled on the History Sources page, or the EnvisionSCADA Historian when no source has it enabled. This is the default of every variable.
EnvisionSCADA HistorianThe samples that EnvisionSCADA records for the variable while Historian is on, also when a project default is set.
A History SourceThat History Source: a query on a Database Connection (Type Database) or the fetch() function of a Python Module script (Type Script).

History Sources Page

Open Configuration > History Sources. The table lists every History Source with its Type, Connection / Module Script, History Reference, History Parameters, Project default and Used by, the number of variables that read their history from the source. Variables that follow the project default are counted for the source that is the project default. Add opens the Add History Source dialog and the Settings button of a row opens the Edit History Source dialog. Name, History Reference, History Parameters, Project default and Description can also be edited in the table.

SettingDescriptionExample
NameThe name that the variables select. Letters, digits and underscore. Renaming a source changes the History Source of every variable and UDT member that selects it, in the same save.PlantPI
DescriptionFree text.PI Data Archive of the plant
TypeScript calls the fetch() function of a Python Module script. Database reads the history with a query on a Database Connection.Script
Module ScriptShown for Script. The Python Module script that defines fetch(). The list shows the Python scripts with IsModule enabled.pi_trend
ConnectionShown for Database. The Database Connection that holds the history. The list shows the SQL Server, MySQL, SQLite, PostgreSQL and InfluxDB 3 connections of the Database Connections page. For an InfluxDB 3 connection the History Query is InfluxDB SQL, see Database on InfluxDB 3.ProductionSQL
History ParametersShown for Script. Optional key=value pairs separated by ;, passed to fetch() for every variable of the source. A variable adds its own History Parameters; a key that the variable sets replaces the same key of the source.mode=plot
History QueryShown for Database. The SELECT that returns the history. When empty, the default query is used, see Database.SELECT TimeStamp, Value FROM HistoricData WHERE Variable = @Variable AND TimeStamp BETWEEN @Start AND @End ORDER BY TimeStamp
History ReferenceThe History Reference of every variable of this source that has none of its own. When empty, each variable is read under its full name. {Name} is replaced by the full name of the variable, so BIO_{Name} reads the variable Pump1_Speed as BIO_Pump1_Speed.BIO_{Name}
Project defaultVariables whose History Source is Project default read from this source. One source at a time is the project default: enabling it on a source disables it on the others. Without a project default, these variables read the EnvisionSCADA Historian.On
Test with variable, TestTest reads the last 24 hours for the variable named in Test with variable, with the values in the dialog, saved or not. It shows the number of rows, the time the read took and the History Reference that was used, or the reason the read failed.2001 rows over the last 24 h, returned in 32 ms (DEMO_SINE)

The settings are checked when the source is saved:

  • The Name is unique. Historian, Database and Script are reserved.
  • A Database History Source needs a Connection, a Script History Source a Module Script. The Database Connection or the Python Module script must exist. The Database Connection is a SQL Server, MySQL, SQLite, PostgreSQL or InfluxDB 3 connection.
  • The History Query must start with SELECT or WITH.
  • History Parameters must be key=value pairs separated by ';'.

A History Source that variables or UDT members select is kept until they select another History Source: Delete then reports how many variables and UDT members still use it. The project default is kept until Project default is disabled on it.

History Source Settings of a Variable

The History Source section is at the bottom of the Historian tab of the variable dialog. History Source lists Project default, EnvisionSCADA Historian and the History Sources of the project. Project default shows the current project default between brackets, for example Project default (PlantPI), or Project default (EnvisionSCADA Historian) when no source is the project default. For a History Source, the dialog shows its Type and its Module Script or Connection, with a link to the History Sources page.

History Reference and History Parameters belong to the variable. An empty field shows, in italics, what the History Source fills in. Typing a value gives the variable its own value, and Reset empties the field again.

SettingDescriptionExample
History SourceProject default, EnvisionSCADA Historian or a History Source of the History Sources page.PlantPI
History ReferenceWhat the History Source reads for this variable: the reference passed to fetch(), for example the tag name in an external historian, or @Variable of the History Query. When empty, the History Reference of the source is used. {Name} is replaced by the full name of the variable. Browse lists the references that a Script source offers; a click on a reference fills the field.CDT158
History ParametersShown for a Script source. Optional key=value pairs separated by ;, added to the History Parameters of the source. A key set here replaces the same key of the source.scale=0.1
TestReads the last 24 hours with the values in the dialog, saved or not, and shows the number of rows, the time the read took and the last sample, or the reason the read failed.2001 rows over the last 24 h, returned in 11 ms

The variable pages offer the same settings as table columns: History Source, History Reference and History Parameters. Show them with Columns as described in the Configuration Tables article. An empty cell shows, in italics, what the variable takes from its History Source. In the History Source column, an empty cell is Project default and Historian is the EnvisionSCADA Historian.

The settings are checked when the variable is saved:

  • History Source must be empty (Project default), Historian (EnvisionSCADA Historian) or the name of a History Source. A History Source that is selected must exist.
  • History Parameters must be key=value pairs separated by ';'.

Database

A History Source of Type Database runs its History Query on its Database Connection each time a trend reads the history of a variable. The query returns two columns: the time first and the value second. The time can be a date and time value, a date and time with offset, text in ISO 8601 format, or a Unix time in seconds or milliseconds. Text without a time zone is read as UTC. At most 500,000 rows are read per request, like the EnvisionSCADA Historian.

PlaceholderValue
@VariableThe History Reference of the variable, by default its full name, for example Pump1_Speed.
@StartThe start of the requested range, in UTC.
@EndThe end of the requested range, in UTC.

When History Query is empty, the default query reads back the HistoricData table that the default data template of a Data Forwarder writes. The last value before the start of the range is added at the start, as the EnvisionSCADA Historian does, so the line starts at the left edge of the trend.

Default query
SELECT TimeStamp, Value FROM HistoricData
WHERE Variable = @Variable AND TimeStamp BETWEEN @Start AND @End
ORDER BY TimeStamp

The query decides how far a long range is thinned. The example below reads the same table on a SQLite connection and returns one averaged row per minute; change 60 to the number of seconds per row.

One averaged row per minute (SQLite)
SELECT MIN(TimeStamp), AVG(CAST(Value AS REAL)) FROM HistoricData
WHERE Variable = @Variable AND TimeStamp BETWEEN @Start AND @End
GROUP BY CAST(strftime('%s', TimeStamp) / 60 AS INTEGER)
ORDER BY 1
  • A SQLite connection to an EnvisionSCADA database (Configuration.db, Historian.db, Local.db or StandbyHistorian.db) is refused when the history is read. The samples that EnvisionSCADA records are read with the EnvisionSCADA Historian.
  • The query runs for every user who opens a trend. Use a database login with read access.
  • A query that does not answer within 60 seconds is stopped and reported as the reason.

Database on InfluxDB 3

A History Source of Type Database also reads an InfluxDB 3 connection, a connection whose Connection String has the key Database; see the Database Connections article. The History Query is InfluxDB SQL with the same placeholders @Variable, @Start and @End, and returns the time first and the value second, as on the other databases. The Connection list shows the InfluxDB 3 connections; an InfluxDB 2.x connection, with Org and Bucket, serves Data Forwarders.

When History Query is empty, the default query reads back what the default InfluxDB data template of a Data Forwarder writes: the measurement historian, with the variable name in the tag variable and the value in the field value. The last value in the 24 hours before the start of the range is added at the start, so the line starts at the left edge of the trend.

A Data Forwarder writes a text value to the field value_text. To read text values, use a History Query that selects value_text.

Default query (InfluxDB 3)
SELECT time, value FROM historian
WHERE variable = @Variable AND time >= @Start AND time <= @End
ORDER BY time

The query decides how far a long range is thinned. The example below returns one averaged row per minute; change 1 minute to the interval per row.

One averaged row per minute (InfluxDB 3)
SELECT date_bin(INTERVAL '1 minute', time) AS time, avg(value) AS value FROM historian
WHERE variable = @Variable AND time >= @Start AND time <= @End
GROUP BY 1
ORDER BY 1
  • The Token of the connection needs read access to the database.
  • InfluxDB 3 Core limits the number of Parquet files that one query reads (the query file limit of the server). A long range can reach that limit; the trend then shows the message of InfluxDB as the reason.
  • A query that does not answer within 60 seconds is stopped and reported as the reason.

Script

A History Source of Type Script calls the fetch() function of its Module Script, a Python script with IsModule enabled; see the Python Scripts article. The script runs in the EnvisionSCADA Python engine and can use the Python libraries installed for EnvisionSCADA, for example the client library of an external historian.

Module script functions
def fetch(references, start, end, params):
    # references  the History Reference of every variable in the call
    # start, end  the requested range, as datetime in UTC
    # params      the History Parameters as a dictionary
    return {"DEMO_SINE": [(timestamp, value), ...]}

def browse(search):
    # optional: the references Browse offers
    return [{"reference": "DEMO_SINE", "unit": "degC", "description": "Demo sine"}]
ArgumentDescription
referencesA list with the History Reference of every variable in the call.
start, endThe requested range, as Python datetime values in UTC.
paramsThe History Parameters of the source with those of the variable added, as a dictionary, for example {"mode": "plot", "scale": "0.1"} for mode=plot on the source and scale=0.1 on the variable.

fetch() returns a dictionary with one entry per reference. Each entry is a list of (timestamp, value) pairs, or of dictionaries with the keys timestamp and value. A timestamp is a Python datetime (without a time zone it is read as UTC), ISO 8601 text, or a Unix time in seconds or milliseconds. A value is a number, a boolean or text. A point with the value None or NaN is left out. Start each list with the last value before start, so the line starts at the left edge of the trend.

The script decides how many points it returns. For a long range, read a thinned series where the external system offers one, for example about 2000 points, the width of a trend on a full HD screen. A trend shows at most 500,000 points per variable.

One fetch() Call for Several Variables

The Trend Analyst and Quick Trend read all pens at the same moment. Variables that use the same Module script with the same History Parameters, those of the source with those of the variable added, read over the same range, are answered by one fetch() call, and references holds the History Reference of each of them. A script can therefore read all tags of a trend in one request to the external system. Variables with different History Parameters are read in separate calls.

A reference that is missing from the returned dictionary is reported for its variable: Script '<script>' returned nothing for reference '<reference>'. An exception in fetch() is reported for every variable of the call, with the exception type and message. A call that does not return within 60 seconds is reported as an error.

Browse

browse(search) is optional. Browse in the History Source section of a variable calls browse() of the Module Script of the selected source with the text in History Reference and lists the references it returns, with their unit and description. A click on a reference fills History Reference. browse() returns a list of dictionaries with the keys reference, unit and description, or a list of texts.

When the script has no browse() function, Browse shows: This script has no browse() function. Type the reference.

Example Module Script

The Module script below returns calculated signals, to try a Script source without an external system. Create it as a Python script named history_demo with IsModule enabled, and add a History Source of Type Script with Module Script history_demo on the History Sources page. Select that source on a variable and set History Reference to DEMO_SINE, DEMO_SLOW, DEMO_FAST or DEMO_FLAT. History Parameters period (seconds of one sine) and step (seconds between points) are optional, on the source for all its variables or on one variable.

The reference FAIL makes fetch() raise an exception and a reference that starts with UNKNOWN is left out of the result, which shows how the trends report both cases.

history_demo
import math

# Every call is counted, so a test can see that pens of one trend round share one call.
CALLS = []

# A long range is thinned to about this many points, the width of a screen.
POINTS = 2000

_KNOWN = {
    "DEMO_SINE": ("Demo sine", "degC", 50.0, 20.0),
    "DEMO_SLOW": ("Demo slow wave", "bar", 2.0, 0.5),
    "DEMO_FAST": ("Demo fast wave", "rpm", 1500.0, 300.0),
    "DEMO_FLAT": ("Demo flat line", "%", 42.0, 0.0),
}


def _signal(reference, t, period):
    description, unit, offset, amplitude = _KNOWN.get(reference, (reference, "", 10.0, 5.0))
    phase = (sum(ord(c) for c in reference) % 360) * math.pi / 180.0
    return round(offset + amplitude * math.sin(2 * math.pi * t / period + phase), 3)


def fetch(references, start, end, params):
    CALLS.append(list(references))
    del CALLS[:-50]   # the module stays loaded: keep only the latest calls
    if "FAIL" in references:
        raise RuntimeError("history_demo was asked to fail for reference FAIL")

    period = float(params.get("period", 600))
    span = (end - start).total_seconds()
    # Never more than POINTS points, never closer than step seconds.
    step = max(float(params.get("step", 10)), span / POINTS)

    result = {}
    t0 = start.timestamp()
    for reference in references:
        if reference.startswith("UNKNOWN"):
            continue
        points = []
        t = t0 - step   # the last value before start: the line starts at the left edge
        while t <= t0 + span:
            points.append((t, _signal(reference, t, period)))
            t += step
        result[reference] = points
    return result


def browse(search):
    search = (search or "").upper()
    return [{"reference": name, "unit": unit, "description": description}
            for name, (description, unit, _o, _a) in sorted(_KNOWN.items())
            if search in name]

Example: Historian with a REST API

Many historians offer a REST API. The two scripts below show the pattern with a dummy historian, so the chain can be tried without an external system. dummy_rest_historian.py stands in for the historian: a small web server, started outside EnvisionSCADA, that calculates a signal for the tags DUMMY_FLOW, DUMMY_LEVEL and DUMMY_TEMP. rest_trend is the Module script that reads it.

dummy_rest_historian.py
# Dummy REST historian for the EnvisionSCADA History Sources example.
#
# Stands in for the REST API of an external historian: it calculates a signal for each tag, so the
# rest_trend Module script can be tried without a real historian. Start it on the EnvisionSCADA server
# with the Python that EnvisionSCADA uses:
#
#     python dummy_rest_historian.py
#
# It listens on http://127.0.0.1:8099 and answers two requests:
#
#     GET /tags?search=FLOW
#         ["DUMMY_FLOW"]
#     GET /history?tags=DUMMY_FLOW,DUMMY_TEMP&start=2026-10-21T08:00:00Z&end=2026-10-21T09:00:00Z&points=1000
#         {"DUMMY_FLOW": [["2026-10-21T07:59:56Z", 118.2], ...], "DUMMY_TEMP": [...]}
#
# A tag it does not know is left out of the answer. Stop it with Ctrl+C.

import datetime
import json
import math
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse

PORT = 8099

# tag: (offset, amplitude, period in seconds)
TAGS = {
    "DUMMY_FLOW": (120.0, 15.0, 900),
    "DUMMY_LEVEL": (55.0, 20.0, 3600),
    "DUMMY_TEMP": (21.0, 2.5, 1800),
}


def signal(tag, t):
    offset, amplitude, period = TAGS[tag]
    return round(offset + amplitude * math.sin(2 * math.pi * t / period), 3)


def parse_time(text):
    return datetime.datetime.fromisoformat(text.replace("Z", "+00:00"))


def iso(t):
    return datetime.datetime.fromtimestamp(t, datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        url = urlparse(self.path)
        query = parse_qs(url.query)
        if url.path == "/tags":
            search = query.get("search", [""])[0].upper()
            self.reply(200, [tag for tag in sorted(TAGS) if search in tag])
        elif url.path == "/history":
            try:
                start = parse_time(query["start"][0]).timestamp()
                end = parse_time(query["end"][0]).timestamp()
                points = int(query.get("points", ["1000"])[0])
            except (KeyError, ValueError) as ex:
                self.reply(400, {"error": "start, end and points are required: %s" % ex})
                return
            step = max(1.0, (end - start) / max(1, points))
            result = {}
            for tag in query.get("tags", [""])[0].split(","):
                if tag in TAGS:
                    t = start - step   # the last value before start: the line starts at the left edge
                    series = []
                    while t <= end:
                        series.append([iso(t), signal(tag, t)])
                        t += step
                    result[tag] = series
            self.reply(200, result)
        else:
            self.reply(404, {"error": "unknown path " + url.path})

    def reply(self, status, body):
        data = json.dumps(body).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)


if __name__ == "__main__":
    print("Dummy REST historian on http://127.0.0.1:%d (Ctrl+C stops it)" % PORT)
    ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever()

Start the dummy historian on the EnvisionSCADA server with python dummy_rest_historian.py. It answers on http://127.0.0.1:8099 until it is stopped with Ctrl+C.

Create rest_trend as a Python script with IsModule enabled and add a History Source of Type Script with Module Script rest_trend. Select that source on a variable and set History Reference to DUMMY_FLOW, DUMMY_LEVEL or DUMMY_TEMP. Browse lists these tags.

rest_trend
import json
import urllib.error
import urllib.parse
import urllib.request

# The REST API of the historian. The dummy_rest_historian example answers on this address.
BASE_URL = "http://127.0.0.1:8099"

# Sent as "Authorization: Bearer <key>" when not empty.
API_KEY = ""

# EnvisionSCADA waits at most 60 seconds for a history read.
TIMEOUT = 30

# How many points per tag the historian is asked for, whatever the length of the range.
POINTS = 1000


def _get(path, query):
    request = urllib.request.Request(BASE_URL + path + "?" + urllib.parse.urlencode(query),
                                     headers={"Accept": "application/json"})
    if API_KEY:
        request.add_header("Authorization", "Bearer " + API_KEY)
    try:
        with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
            return json.loads(response.read().decode("utf-8"))
    except urllib.error.HTTPError as ex:
        raise RuntimeError("%s answered HTTP %s: %s" % (BASE_URL, ex.code, ex.read().decode("utf-8", "replace")[:200]))
    except urllib.error.URLError as ex:
        raise RuntimeError("%s cannot be reached: %s" % (BASE_URL, ex.reason))


def fetch(references, start, end, params):
    reply = _get("/history", {
        "tags": ",".join(references),
        "start": start.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "end": end.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "points": POINTS,
    })
    # The answer maps each tag to a list of time and value pairs; a tag the historian does not know
    # is not in it, and EnvisionSCADA reports that tag as unknown.
    return {tag: [(time, value) for time, value in series] for tag, series in reply.items()}


def browse(search):
    return [{"reference": tag, "unit": "", "description": "Tag of the REST historian"}
            for tag in _get("/tags", {"search": search or ""})]

For a real historian, set BASE_URL to its address and API_KEY to its key when the API requires one, and adapt the two requests to the paths and the answer of its API. rest_trend asks for 1000 points per tag whatever the length of the range (POINTS). A reference the historian does not know is reported as unknown, and a historian that cannot be reached is reported as the read error of every variable in the call.

UDT Members and Variable Arrays

History Source, History Reference and History Parameters are part of a UDT variable member and belong to the definition, like Historian. A parameter token gives each instance its own value, for example {PiTag} in History Reference. Tokens are resolved when the instance is pushed. An empty History Reference on the member uses the History Reference of the source. In an array member, {Index} in History Reference gives each element its own reference. A change of these settings on a member marks the instances Outdated until they are pushed.

The elements of a variable array take the History Source settings of the first element of the array. With an empty History Reference, or {Name} in a History Reference, every element is read under its own full name. For a reference per element that differs from its name, use a UDT array member with {Index} in History Reference.

Projects from an Earlier Version

In earlier versions each variable held its own history source: Database with a connection and a History Query, or Script with a Module script. When such a project is upgraded or imported, every distinct combination becomes a History Source named after its Module script or Database Connection, for example pi_trend, and every variable and UDT member that used it selects that source. A second History Query on the same connection gets a numbered name, for example ProductionSQL_2. The History Reference and History Parameters of a Script source stay on the variable. No source is made the project default, so every trend reads its history from the same place as before. The System Log lists the History Sources that the upgrade created.

A UDT member whose connection or Module script was a parameter token keeps that token as its History Source, so the token resolves to the name of a History Source.

A UDT type file from an earlier version carries no History Sources. Each member selects the History Source of the project that reads from the same Module script or Database Connection, or else the name of that Module script or connection. The import reports the names for which the project has no History Source; create them on the History Sources page.

Notes

Only the trends read the history source: the Trend Analyst, Quick Trend, the Trend object, and GetHistoricData in the runtime API and MCP. The Historian Viewer, Reports, the historical access of the OPC UA Server and scripts that query Historian.db read the EnvisionSCADA Historian.

Renaming a History Source changes the History Source of every variable and UDT member that selects it, in the same save. A UDT parameter that only feeds History Source follows as well. Renaming a Database Connection or a Module script changes the Connection or Module Script of the History Sources that use it. After renaming a Module script, the rename dialog lists the other places that still use the previous name, such as Execute Script actions and RunScript calls; see the Cross Reference Tool (CTRL+F) article.

The History Sources page, Test and Browse are available to the Administrators group, like the variable configuration.

Something missing or wrong in this article? Contact us