The shift start and end calendars still show yesterday's date, or last week's. The shift-active expression tag goes false after midnight and stays false. Closing and reopening the window makes it right again for one day. On a station that never closes the window, the shift window never moves forward.
Drop the Fixes That Don't Hold
Each of these gets tried first. None survives a 24/7 station.
-
Typing a fixed date into the Popup Calendar (for example
2015/09/29 08:00:00 AM). That is a static value. It stays until someone types another one. -
Binding the calendar to
dateFormat(now(0), "yyyy-M-dd 08:00:00"). This updates only when the window opens.now(0)means a poll rate of zero: evaluate once, never again. -
Raising the poll rate to
now(3600000). The date does roll over within an hour of midnight. But every hourly evaluation writes08:00:00back into the calendar and wipes any time the operator entered. - Stretching the poll to 8 hours or more. Fewer overwrites, but the date can lag midnight by up to the full poll interval. You trade one fault for another.
-
Comparing hours only:
dateExtract(now(), "hour") >= {[~]start hour} && dateExtract(now(), "hour") <= {[~]end hour}. It works for whole hours. It has no minute resolution. Because of the<=, an end hour of 16 keeps the tag true until 16:59:59.
Understand Why the Date Freezes
An expression binding re-evaluates for two reasons: a referenced property or tag changes, or its poll timer fires. now() takes a poll rate argument in milliseconds. That argument is the only thing that makes a time-based expression run again on a window nobody touches.
-
now(0): no poll. It evaluates when the binding starts, which means when the window opens. -
now(1000): it re-evaluates every second. -
now(3600000): it re-evaluates every hour.
A property binding is also one-way. The expression owns the property. Every evaluation overwrites whatever the operator put there. A live date and an operator-edited time cannot share one bound calendar property. That is the actual conflict, and no poll rate resolves it.
| Symptom | Cause |
|---|---|
| Date never changes on a window left open | Static entry, or binding uses now(0)
|
| Date correct only after reopening the window |
now(0) evaluates once at binding start |
| Operator's shift time snaps back to 08:00:00 | Polling binding overwrites the bound property on every evaluation |
| Date changes hours after midnight | Poll interval too long (8 h or more) |
| Shift tag ignores minutes, stays true through the end hour | Hour-only dateExtract comparison with inclusive <=
|
Split the Date From the Time
Start here. Apply this rule: now() supplies the date and nothing else. The operator supplies hour, minute and second in separate numeric fields. An expression joins the two into a timestamp string. The shift tag compares full timestamps.
The date part changes only at midnight. The time part changes only when the operator edits it. The poll can run every second and never overwrite operator input, because the operator never edits the bound property.
Build the Shift Window
- Add numeric entry fields for hour, minute and seconds. Use one set for the shift start and one set for the shift end. The working build used components named
hour,minuteandsecondson the Root Container. - Add a label and bind its text to the composed timestamp:
dateFormat(now(), "yyyy-M-dd " + {Root Container.hour.intValue} + ":" + {Root Container.minute.intValue} + ":" + {Root Container.seconds.intValue})now()with no argument keeps polling. Only the date comes from it. The time fields come from the operator. - Repeat step 2 for the end boundary using the end-time fields.
- Write each composed string into its tag,
startDateandendDate. The shift expression tag reads these tags. - Create the shift-active expression tag:
now() >= todate({[~]startDate}) && now() <= todate({[~]endDate})
You may only need a time of day, with no date. In that case, skip the strings and compare minutes since midnight. The tag names below are placeholders. Substitute your own.
(dateExtract(now(), "hour") * 60 + dateExtract(now(), "minute")) >= ({[~]<start hour tag>} * 60 + {[~]<start minute tag>})
&&
(dateExtract(now(), "hour") * 60 + dateExtract(now(), "minute")) < ({[~]<end hour tag>} * 60 + {[~]<end minute tag>})
Verify the Rollover Without Waiting for Midnight
- Shrink the test to one minute. Bind a scratch label to
dateFormat(now(1000), "yyyy-M-dd 08:mm:00"). The minute field should tick once per minute while the rest of the string stays fixed. This proves that a pollednow()updates on a window left open. - Set the start time two minutes ahead and the end time four minutes ahead. Watch the shift tag go true at the start boundary and false after the end boundary.
- Check the quality of the shift tag, not just its value. If
todate()cannot parse the string, the tag shows an error quality instead of a clean false. - Leave the window open across one real midnight. Confirm that the label date increments and the operator-entered time stays put.
Avoid the Recurring Traps
-
Overnight shifts. If the end time is earlier than the start time and both strings carry today's date, the window is never true. Compose the end with the next day's date. In the minutes-of-day version, swap the
&&for||when the end is less than the start. -
Unpadded fields. The concatenation produces strings like
2015-9-29 8:5:0. Confirm thattodate()parses your exact format on your system. If it fails, pad each field to two digits withnumberFormat(value, "00"). - Two clocks. The label runs in the client and uses the client's clock. The expression tag runs on the gateway and uses the gateway's clock. If the time zones or clocks differ, the shift boundaries shift by that offset. Compare both clocks before you blame the expression.
-
Inclusive end. With
<=, the tag stays true through the end second. If the next shift starts at that same second, both shift tags are true together. Use<on the end boundary. -
Binding the calendar itself. Never bind an operator-editable calendar to a polling
now(). Keep entry fields unbound, and put the binding on the composed label or tag.
FAQ
What happens if I use now(0) in an Ignition expression binding?
It evaluates once, when the binding starts, which usually means when the window opens. It never evaluates again. On a window left open around the clock, the date stays frozen until someone closes and reopens the window.
What happens if I set now() to poll every hour on a popup calendar binding?
The date rolls over within an hour of midnight. However, every poll writes the fixed time, such as 08:00:00, back into the calendar and wipes operator edits. Move the operator's time into separate unbound numeric fields and join them with the now() date in a label or tag.
What happens if the shift tag still won't roll over after the fix?
First check the tag quality, the client and gateway clocks, and whether todate() parses the composed string. If the label date increments at midnight but the tag stays stale with good quality, or todate() rejects a string that matches the format, stop rewriting expressions. Record the gateway version, the exact expressions, the tag diagnostics and the related gateway log entries, then open a case with Inductive Automation support.