Reading S7-300 DATE Data Type in Node-RED with node-red-contrib-s7
The Siemens S7-300 CPU family encodes time information in several distinct elementary data types (DATE, TIME_OF_DAY, DATE_AND_TIME, DTL), each with a different footprint and interpretation. The 2-byte DATE type is the simplest of the group, yet it is the source of the most common conversion errors when consumed outside STEP 7, especially when the consumer is a Node-RED flow rather than a TIA Portal HMI. This reference documents the exact bit layout, the S7 epoch, the integer-to-date conversion, and a verified implementation using node-red-contrib-s7 published on the Node-RED Flow Library.
1. S7 DATE Data Type Specification
Per the Siemens STEP 7 reference help, the elementary data type DATE occupies 2 bytes (16 bits) in the PLC memory and is stored as an unsigned integer. The value represents the number of days that have elapsed since 1990-01-01 (inclusive of 1990-01-01 = 0). The data type is defined in IEC 61131-3 and behaves identically across all S7 CPU families.
| Property | Value |
|---|---|
| Byte width | 2 bytes (16 bits) |
| Encoding | Unsigned integer, big-endian byte order |
| Value range | 0 ... 65535 |
| Epoch | 1990-01-01 (= 0 days) |
| Maximum representable date | 1990-01-01 + 65535 d = 2169-06-06 |
| Minimum representable date | 1990-01-01 |
| Resolution | 1 day (no time-of-day component) |
The byte layout in the PLC data block is illustrated below. The high byte carries bits 15..8, the low byte carries bits 7..0. Siemens S7 always stores multi-byte values in big-endian (network byte order).
Example reference values:
| Value (hex) | Value (dec) | Date |
|---|---|---|
| 0x0000 | 0 | 1990-01-01 |
| 0x0001 | 1 | 1990-01-02 |
| 0x29D2 | 10706 | ~ 2019-04-24 (field-verified) |
| 0xFFFF | 65535 | ~ 2169-06-06 |
2. Why the DATE Type Trips Up Node-RED Users
The S7 ISO-on-TCP (RFC 1006) transport delivers raw register bytes. The node-red-contrib-s7 node, derived from the ST-One project, decodes most elementary types (BOOL, INT, REAL, BYTE, WORD, DWORD, STRING, CHAR, S5TIME) but cannot natively emit a JavaScript Date object for the DATE type, because the S7 DATE epoch (1990-01-01) differs from the JavaScript epoch (1970-01-01). The node delivers a numeric payload; the conversion has to happen in a function node.
Three pitfalls are observed in the field:
-
Byte-order reversal: Reading the two DATE bytes as two separate BYTE tags produces low byte and high byte. If the function node is fed the low byte first, the resulting integer is wrong by a factor of up to 256. The correct approach is to either read the full WORD (16-bit) in a single tag, or to assemble the bytes as
(hi << 8) | lo. -
Epoch offset missing: JavaScript
Datecounts milliseconds from 1970-01-01, while S7 DATE counts days from 1990-01-01. The conversion must add the 7305-day offset between the two epochs. -
Time-zone leakage: JavaScript
Dateis locale-aware. Always normalize withtoISOString()or usegetUTC*accessors to avoid local-time drift that can flip the day in non-UTC deployments.
3. Prerequisites
- Node.js 14+ (LTS recommended) and Node-RED 2.x or 3.x installed on the host.
- A Siemens S7-300 CPU with an Ethernet CP (CP 343-1 Lean/Standard/Advanced) or a CPU 31x-2 PN/DP. Older MPI/DP-only CPUs require a NetLink or IBH Link gateway.
- DB tag of type
DATEdefined in the S7 project. Example path:DB100.DBD0(DWORD) orDB100.DBW0(WORD) holds two adjacent DATE values, or a singleDB100.DBW0holds a single DATE. -
node-red-contrib-s7 installed via the palette manager or
npm install node-red-contrib-s7. - CPU protection level set to allow PUT/GET (S7-300: Properties > Protection > Permit access with PUT/GET).
4. node-red-contrib-s7 Configuration
After installing the package, drag the s7 in node onto the canvas and open its configuration dialog. The host, rack, and slot parameters must match the S7-300 CPU's TCP/IP address and physical position.
| Parameter | S7-300 Typical Value | Notes |
|---|---|---|
| Address (IP) | 192.168.0.10 | Static IP of CP 343-1 or PN interface |
| Port | 102 | ISO-on-TCP (RFC 1006) default |
| Rack | 0 | Hard-wired for S7-300 |
| Slot | 2 | CPU slot; verify with HW Config |
| Mode | All / Single variable | Single recommended for one DATE tag |
| Variable | DB100,WORD0 | Read the 2-byte DATE as a single WORD |
| Cycle | 1000 ms | Poll interval; 1 s is adequate for day-resolution DATE |
If the tag must be read from a S5TIME-aligned area where the WORD overlaps other types, use the bytewise variant with two BYTE reads (DB100,BYTE0 and DB100,BYTE1) and recombine them. This pattern is required when the surrounding DB structure packs a DATE inside a larger UDT or when the byte address is odd.
5. The Conversion Formula
The relationship between an S7 DATE value N and a JavaScript Date is:
t_js_ms = (N + 7305) × 86400000
date_js = new Date(t_js_ms)
Where:
- N = S7 DATE value, integer in [0, 65535].
- 7305 = number of days from 1970-01-01 to 1990-01-01 (20 years × 365 + 5 leap days in 1972, 1976, 1980, 1984, 1988).
- 86400000 = milliseconds per day.
The constant 7305 is verified as follows:
| Period | Days |
|---|---|
| 1970-01-01 to 1980-01-01 | 3653 (3 leap years: 1972, 1976, 1978*) |
| 1980-01-01 to 1990-01-01 | 3652 (2 leap years: 1980, 1984, 1988) |
| Total | 7305 |
6. Step-by-Step Implementation
-
Add the S7 input node. Drag the s7 in node, point it at the DATE tag (e.g.
DB100,WORD0), and connect it to a debug node. Confirmmsg.payloadis a number 0..65535. This is the integer day count emitted by the S7 node. -
Add a function node. Paste the conversion code shown in section 7. The function takes
msg.payloadas the S7 days count and emits an ISO date string onmsg.payload. - Wire the function to a downstream sink. Typical targets: debug for verification, a change node feeding an mqtt out node for telemetry, or a MSSQL writer for archival.
-
Deploy and observe. The debug pane should show a string such as
2019-04-24for an S7 value of 10706. -
Validate against a known S7 variable. In STEP 7, write
DATE#2019-04-24to the same DBW, then read the value back in Node-RED. The output must match the literal date written.
7. Complete Function Node Code
Drop the following JavaScript into a Node-RED function node. The code handles both single-WORD and bytewise input, validates the range, normalizes the time zone to UTC, and returns a clean ISO 8601 date string.
// S7-300 DATE → JavaScript Date conversion
// Input: msg.payload = unsigned 16-bit integer, days since 1990-01-01
// Output: msg.payload = ISO 8601 date string (YYYY-MM-DD, UTC)
const S7_EPOCH_DAYS = 7305; // 1970-01-01 → 1990-01-01
const MS_PER_DAY = 86400000;
const S7_MAX_DAYS = 65535; // 0xFFFF
let n = msg.payload;
// Accept both number and string-numeric input
if (typeof n === 'string') n = parseInt(n, 10);
if (typeof n !== 'number' || !Number.isFinite(n)) {
node.warn('Non-numeric S7 DATE payload: ' + JSON.stringify(msg.payload));
return null;
}
if (n < 0 || n > S7_MAX_DAYS) {
node.warn('S7 DATE out of range: ' + n);
return null;
}
// 7305 days corrects the 1970/1990 epoch gap
const ms = (n + S7_EPOCH_DAYS) * MS_PER_DAY;
const d = new Date(ms);
// YYYY-MM-DD in UTC, immune to host time-zone
msg.payload = d.toISOString().substring(0, 10);
msg.s7_raw = n;
msg.js_date = d.toISOString();
return msg;
For a bytewise read where two separate BYTE tags are pulled, recombine them upstream:
// Recombine two BYTE values into a single 16-bit unsigned word
// msg.payload = [ lowByte, highByte ] (low byte FIRST in the array)
const [lo, hi] = msg.payload;
msg.payload = (hi << 8) | lo;
return msg;
Flow topology (textual representation, deployable in Node-RED):
8. Verification Procedure
- Open STEP 7 (or TIA Portal) and the watch table tied to the same DBW. Set a breakpoint, force
DB100.DBW0 = W#16#0001, and observe the PLC value. - In Node-RED, deploy the flow and read the debug pane. The expected output is
1990-01-02. - Repeat the test with
W#16#0000(expect1990-01-01) and with a value that maps to a recent year, for exampleW#16#29D2= 10706 (expect2019-04-24± 1 day depending on leap-year handling at the boundary). - Compare the function-node output with the human-readable conversion
1990-01-01 + 10706 daysin a spreadsheet using=DATE(1990,1,1)+10706. The two must match exactly.
9. Edge Cases and Pitfalls
9.1 Boundary Years
Because the DATE field crosses two 100-year blocks, the conversion is sensitive to how the host JS engine handles years before 1900. Modern V8 (Node.js 14+) and SpiderMonkey handle four-digit years correctly, so a value of 0 returns 1990-01-01 without issue. Avoid older Node.js (pre-12) where Date had legacy two-digit year handling.
9.2 DST and Time Zone
Node-RED inherits the OS time zone. If the host is set to a region with DST, naive Date formatting can shift the day by ±1 in the 23:00-01:00 window. Always emit the date with toISOString() (UTC) and slice the leading 10 characters, or call getUTCFullYear, getUTCMonth, getUTCDate explicitly.
9.3 Byte-Swap and Endianness
The S7 ISO transport presents bytes in big-endian order. If you poll the two DATE bytes as separate BYTE tags and feed them into the function in the wrong order, the resulting integer is multiplied or divided by 256. Verify by forcing a value of W#16#0100 = 256. If the function returns 1 instead of 256, swap hi and lo in the recombination code.
9.4 DB Re-numbering After Compile
STEP 7 and TIA Portal re-number DB slots when a new block is inserted. A flow that points at DB100,WORD0 can silently break if the project is recompiled with a different DB layout. Always reference DBs by the symbolic name when the project allows it, or rebuild the watch table after every PLC download.
9.5 PUT/GET Blocked on the CPU
On S7-300 CPUs with firmware < V3.x, the CPU rejects external read/write unless the protection level is set to Permit access with PUT/GET communication from remote partner. Symptom: the s7 node logs a connection error and never delivers a payload. The fix is in the CPU properties, not in Node-RED.
10. Related S7 Time Data Types
For context, the S7 family defines four time-related elementary types. The conversion logic differs for each; do not cross-apply them.
| Type | Bytes | Content | Conversion to JS |
|---|---|---|---|
| DATE | 2 | Days since 1990-01-01, UINT | (N + 7305) × 86400000 ms |
| TIME_OF_DAY (TOD) | 4 | Milliseconds since 00:00, UDINT | new Date(N) |
| DATE_AND_TIME (DT) | 8 | BCD year/month/day/hour/min/sec + ms nibbles | BCD decode, ms = ((bcd&0xF0)>>4)*10 + (bcd&0x0F) |
| DTL | 12 | Year, month, day, weekday, hour, min, sec, ms (each 2 or 4 bytes, big-endian) | Use new Date(Date.UTC(year, month-1, day, hour, min, sec, ms)) |
DTL was introduced with S7-1500 and is rarely used on S7-300. On S7-300 the DT type remains the standard 8-byte BCD timestamp.
11. Troubleshooting Matrix
| Symptom | Likely Cause | Fix |
|---|---|---|
payload is a Buffer, not a number |
Variable configured as BYTE / BYTE_ARRAY | Reconfigure s7-in variable as DB100,WORD0
|
| Date is off by exactly 7305 days | Epoch offset missing | Add the 7305-day constant in the function |
| Date is off by one day, intermittent | Local time-zone / DST | Use toISOString() or UTC accessors |
| Date is shifted by 256× or 1/256× | Byte-order reversed on bytewise read | Swap hi / lo in recombination |
| Node-red-contrib-s7 status: connecting, no payload | PUT/GET disabled on CPU | Enable PUT/GET in CPU protection properties |
| Payload is always 0 | Wrong DBW address after recompile | Re-verify address in HW Config / watch table |
| Payload is a string of digits, not a number | Mode configured as string | Reconfigure as numeric WORD |
| Value jumps at 0x7FFF / 0x8000 | Signed INT interpreted as DATE | Reconfigure as WORD (unsigned 16-bit) |
| NaN-Date in ISO output | Negative value reaching the function | Reject with range check (0..65535) |
12. Performance and Polling Notes
Each s7 in node opens a single ISO-on-TCP connection to the CPU. Polling 1 Hz for a single DATE word is well within the S7-300 communication load budget (CP 343-1 supports ~50 connections, but only one is needed). For higher rates (10 Hz+), batch multiple tags into the same s7 in node using the All variables mode rather than opening additional nodes.
If the DATE is consumed by an MQTT publisher, set the MQTT topic to a stable schema such as plant/line1/plc1/date with a retained message and QoS 0, and let downstream consumers cache the value. This avoids redundant PLC polling across multiple Node-RED flows.
13. Alternative Controllers and Compatibility
The 2-byte DATE encoding is consistent across the S7 line. The same function-node code works unchanged on:
- S7-300 (all CPU 31x variants) - this article's primary target
- S7-400 (CPU 41x / 41xH) - identical DATE layout
- S7-1200 (CPU 12xx, firmware > V4.x) - identical; TIA Portal uses the same DTL/epoch
- S7-1500 (CPU 15xx) - identical; prefer the native DTL type for new projects
- ET 200S / ET 200pro IM 151 / IM 154 - the DATE is held in the PLC, not the IM
For LOGO! 8 and S7-200, the time-handling is different (LOGO! uses a 16-bit BCD packed time; S7-200 lacks DATE in the IEC sense), so the conversion does not transfer.
14. Summary
The S7-300 DATE data type is a 2-byte unsigned integer counting days since 1990-01-01. The node-red-contrib-s7 node delivers it as a numeric payload; the JavaScript epoch starts 7305 days earlier, on 1970-01-01. A function node of fewer than 20 lines reconciles the two and emits an ISO 8601 date. Field verification with a value of 10706 confirming 2019-04-24 ± 1 day is the recommended commissioning check.
What is the S7-300 DATE data type, exactly?
S7-300 DATE is a 2-byte (16-bit) unsigned integer that stores the number of days since 1990-01-01. The value 0 represents 1990-01-01, the value 1 represents 1990-01-02, and the maximum representable date is 1990-01-01 + 65535 days, approximately 2169-06-06. The encoding is defined by IEC 61131-3 and is identical across the S7-300, S7-400, S7-1200 and S7-1500 families.
How do I add the 7305-day offset in Node-RED?
Place a function node downstream of the s7 in node and execute new Date((msg.payload + 7305) * 86400000).toISOString().substring(0, 10). The constant 7305 accounts for the difference between the JavaScript epoch (1970-01-01) and the S7 DATE epoch (1990-01-01). Always format the output with toISOString() to keep the date in UTC.
Why does my DATE read as two separate bytes instead of a 16-bit value?
The S7-300 ISO transport is byte-addressed. If the s7 in node is configured as BYTE twice, the payload arrives as two separate values that must be recombined. The cleaner approach is to configure a single WORD variable such as DB100,WORD0, which causes the node to assemble the bytes into a 16-bit unsigned integer automatically.
Why is my DATE shifted by one day sometimes?
The host operating system's local time zone is leaking into the conversion. The fix is to use UTC accessors throughout: getUTCFullYear, getUTCMonth, getUTCDate, or the single-call shortcut toISOString().substring(0, 10). This eliminates DST and time-zone drift in the 23:00-01:00 local-time window.
Can I read DATE_AND_TIME (DT) the same way?
No. DT is 8 bytes BCD-encoded and contains year, month, day, hour, minute, second and milliseconds. It is not a day count, and the conversion requires BCD decoding nibble-by-nibble. If you only need the calendar date from a DT, decode the first 4 bytes (year BCD + month BCD + day BCD) and skip the rest. For new S7-1200/1500 projects, prefer the 12-byte DTL type instead of DT, because DTL is byte-aligned and easier to consume in Node-RED.