Reading S7-300 DATE Data Type in Node-RED: S7 Communication Guide

David Krause13 min read
S7-300SiemensTechnical Reference
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

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.

Scope: S7-300 CPUs (31x series) programmed with STEP 7 V5.x or TIA Portal, accessed from a Linux/Windows host running Node-RED. The same logic applies to S7-400 and the S7-1200/1500 DATE implementation, since the encoding is identical across the S7 family.

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).

Byte n+1 (MSB) bits 15..8 Byte n (LSB) bits 7..0 D15 D14 D13 D12 D11 D10 D9 D8 D7 D6 D5 D4 D3 D2 D1 D0

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
Common mistake: The DATE_AND_TIME (DT) type is 8 bytes BCD-encoded and starts at the same epoch, which is why a first attempt with 3 bytes may appear to work. DATE is a different, smaller type: do not borrow the DT decoding logic for it.

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:

  1. 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.
  2. Epoch offset missing: JavaScript Date counts 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.
  3. Time-zone leakage: JavaScript Date is locale-aware. Always normalize with toISOString() or use getUTC* 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 DATE defined in the S7 project. Example path: DB100.DBD0 (DWORD) or DB100.DBW0 (WORD) holds two adjacent DATE values, or a single DB100.DBW0 holds 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
Note on the leap-year arithmetic: 1970..1989 contains exactly 5 leap years (1972, 1976, 1980, 1984, 1988). 20 × 365 + 5 = 7305. Verify against any online epoch-day calculator before deploying.

6. Step-by-Step Implementation

  1. 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. Confirm msg.payload is a number 0..65535. This is the integer day count emitted by the S7 node.
  2. Add a function node. Paste the conversion code shown in section 7. The function takes msg.payload as the S7 days count and emits an ISO date string on msg.payload.
  3. 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.
  4. Deploy and observe. The debug pane should show a string such as 2019-04-24 for an S7 value of 10706.
  5. Validate against a known S7 variable. In STEP 7, write DATE#2019-04-24 to 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):

S7-300 CPU s7 in (DB100,WORD0) function: S7 DATE → ISO debug / mqtt

8. Verification Procedure

  1. 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.
  2. In Node-RED, deploy the flow and read the debug pane. The expected output is 1990-01-02.
  3. Repeat the test with W#16#0000 (expect 1990-01-01) and with a value that maps to a recent year, for example W#16#29D2 = 10706 (expect 2019-04-24 ± 1 day depending on leap-year handling at the boundary).
  4. Compare the function-node output with the human-readable conversion 1990-01-01 + 10706 days in 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.

Back to blog