Why does storeTagHistory reject a numeric or string Timestamp?
A blank Timestamp works because the function stamps each sample with the current time, and a supplied value fails because the argument is typed as a list of Date objects. The milliseconds-since-1970 number you see in the history database is the storage form, not the input form. Jython hands the gateway whatever object you build, and neither an integer nor a string is converted to a Date on the way in.
The typical failed attempt is a list of strings in various standard formats. No string format works, because the problem is the object type, not the text layout. Every attempt to find the right format fails until the value becomes a Date.
Which Timestamp inputs work and which fail?
| Timestamp argument | Result | Reason |
|---|---|---|
| Omitted or blank | Runs; sample stored at call time | Function defaults to now, stored as milliseconds since 1970 |
| Integer milliseconds since 1970 | Error | A number is not a Date object |
| List of strings, any format | Error | Strings are not parsed by the function |
| A single Date, not in a list | Rejected | The argument is a list; the square brackets are the critical part |
[system.date.now()] |
Runs | List containing a Date object |
[date] built with system.date.getDate and system.date.setTime
|
Runs | List containing an arbitrary Date object |
Which way should you build the Date objects?
Build Dates with the system.date.* functions and pick the function by what your source data looks like. Two approaches cover the cases.
| Source data | Conversion | Precision | Notes |
|---|---|---|---|
| Date parts (year, month, day, h, m, s) |
system.date.getDate then system.date.setTime
|
Seconds | Month is 0-indexed in getDate
|
| Epoch milliseconds | system.date.fromMillis |
Milliseconds | Keeps sub-second resolution; confirm the function exists in your version's script help |
| Timestamp strings |
system.date.parse with a format pattern |
Per pattern | Convert every string first, then wrap the results in a list |
Use getDate/setTime when you assemble a time from discrete fields and one-second resolution is enough. Use fromMillis when the samples already carry epoch milliseconds, since it avoids splitting the value into parts and keeps the milliseconds. Use parse only when the source is text, and treat it as the first stage of the same pipeline: string to Date, then Date into the list.
How do you build the call?
- Convert each source timestamp to a Date object. Do not pass the raw string or number.
- Collect the Dates into a Jython list, one Date per sample you are storing.
- Pass that list as the Timestamp argument and leave every other argument as it was in the working blank-timestamp call.
date = system.date.getDate(2018, 6, 29) # year, month (0-indexed), day
date = system.date.setTime(date, 1, 37, 44) # hours, minutes, seconds
system.tag.storeTagHistory(<other arguments unchanged>, [date])
For several samples, build the list in a loop so the position of each Date matches the position of its path and value in the other list arguments:
dates = []
for row in sourceRows:
dates.append(system.date.fromMillis(row[0]))
system.tag.storeTagHistory(<other arguments unchanged>, dates)
Keep the Date list the same length as the values list. A one-element list holding a single Date is still a list and satisfies the argument type.
What still puts samples at the wrong time?
| Symptom in the history data | Cause | Fix |
|---|---|---|
| Sample lands one month later than intended |
getDate month is 0-indexed, so 6 is July, not June |
Subtract 1 from the calendar month before the call |
| Backfilled rows all stamped at run time | Timestamp argument omitted, so the function used now | Pass the Date list |
| Sub-second detail lost |
setTime takes hours, minutes, seconds only |
Build the Date from epoch milliseconds with system.date.fromMillis
|
| Parsed strings shifted by a fixed number of hours | Text has no zone, so it is interpreted in the script's default time zone | Parse with a format that matches the source and check the gateway zone |
| Error persists after conversion | A string or number is still inside the list | Print type(item) for each element before the call |
How do you confirm the rows landed at the supplied times?
- In the Script Console, print each element of the list and check it displays as a date, not a string or number.
- Run the call with a single sample and a timestamp well away from the current time, so a fallback to now is obvious.
- Query the history for that window, for example with
system.tag.queryTagHistoryor a chart bound to the same history provider, and compare the returned timestamp against the Date you supplied. - Repeat with the full batch and confirm the row count in the window equals the number of Dates in the list.
FAQ
Why does storeTagHistory throw an error when I pass milliseconds since 1970?
The Timestamp argument takes a list of Date objects, and an integer is not a Date. Convert the value with system.date.fromMillis and wrap the result in a list.
Why does storeTagHistory need square brackets around the timestamp?
The argument is a list even when you store one sample, so a bare Date is rejected. Write [date] for a single sample.
Why does my backfilled sample land in the wrong month?
system.date.getDate counts months from 0, so getDate(2018, 6, 29) is a July date. Pass the calendar month minus 1.
Why does leaving Timestamp blank write the current time?
With no timestamp supplied, the function stamps the sample with now and stores it as milliseconds since 1970. Historical backfill needs an explicit Date list to keep the original sample times.