Shifts
In this section you'll configure the plant's shift schedule and learn how shifts provide time context for analysis — how they differ from production schedules and how they are inherited through the asset hierarchy.
Using this shift specification:
The plant generally runs a Day Shift from 7:00 AM - 3:00 PM and an Evening Shift from 3:00 PM - 11:00 PM, Monday through Friday.
Manual Packaging Lines have their own Manual Pack Shift from 9:00 AM - 5:00 PM on Thursdays and Fridays.
- Navigate to Ops → Scheduling → Shifts.
- On the
Philadelphiasite, configure Day Shift, repeating Monday to Friday. - On the
Philadelphiasite, configure Evening Shift, repeating Monday to Friday. - On
Manual Pack 1, configure Manual Pack Shift, repeating Thursday to Friday.
Copy this script into the Ignition Designer Script Console (Tools → Script Console) and run it. It checks that each shift is created on the right asset, that Machine 1 and Spray Line 1 inherit the site shifts while Manual Pack 1 uses only its own, and that each shift repeats on the right days at the right times (in the site's time zone). Each check prints PASS or FAIL with what to fix. It doesn't change anything.
Show the check script
# Kanoa Ops Training - check "Shifts" (Scheduling, lesson 1)
#
# Run in the Ignition Designer Script Console (Tools > Script Console), in the
# project that inherits KANOA_SCRIPTS. It only reads; it changes nothing.
# Every check prints PASS or FAIL with what to fix, then a summary.
from java.util import Calendar, TimeZone, Date
SITE = 'CoreForge\\Philadelphia'
MANUAL_PACK = SITE + '\\Finishing\\Manual Pack 1'
WEEKDAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
SHIFTS = [
# shift, where it's created, days, start, end (site time)
('Day Shift', SITE, WEEKDAYS, '07:00', '15:00'),
('Evening Shift', SITE, WEEKDAYS, '15:00', '23:00'),
('Manual Pack Shift', MANUAL_PACK, ['Thu', 'Fri'], '09:00', '17:00'),
]
# assets that should inherit the site shifts, so must have no shifts of their own.
# Manual Pack 1 has its own shift, so it doesn't inherit them.
INHERIT = [SITE + '\\Machining', SITE + '\\Coating', SITE + '\\Finishing',
SITE + '\\Machining\\Machine 1', SITE + '\\Coating\\Spray Line 1']
WINDOWS_TZ = {
'eastern standard time': 'America/New_York', 'central standard time': 'America/Chicago',
'mountain standard time': 'America/Denver', 'pacific standard time': 'America/Los_Angeles',
}
DAY_NAMES = {Calendar.MONDAY: 'Mon', Calendar.TUESDAY: 'Tue', Calendar.WEDNESDAY: 'Wed',
Calendar.THURSDAY: 'Thu', Calendar.FRIDAY: 'Fri', Calendar.SATURDAY: 'Sat', Calendar.SUNDAY: 'Sun'}
ORDER = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
results = []
def check(ok, what, fix=None):
results.append(bool(ok))
if ok:
print 'PASS %s' % what
else:
print 'FAIL %s' % what
if fix:
print ' -> %s' % fix
def field(row, *names):
for n in names:
if n in row and row[n] is not None:
return row[n]
return None
def text(v):
return '' if v is None else unicode(v).strip()
def squash(v):
return ' '.join(text(v).lower().split())
def norm(path):
return text(path).replace('/', '\\').strip('\\')
def rows(fn, params):
# Kanoa returns a dataset, a list, or None when there's nothing to return
data = fn(params)
if data is None:
return []
if hasattr(data, 'getRowCount'):
# read the dataset directly so date columns stay dates
cols = list(data.getColumnNames())
return [dict((c, data.getValueAt(i, c)) for c in cols) for i in range(data.getRowCount())]
return [dict(r) for r in data]
def isPlaceholder(name):
# assets with no shifts of their own report a 'No Shift Defined' row
return squash(name) in ('', 'no shift defined')
def showColumns(label, data, key):
if data and key not in data[0]:
print 'NOTE %s has no %s column. Columns: %s' % (label, key, ', '.join(sorted(data[0].keys())))
def toDate(v):
if v is None:
return None
if isinstance(v, Date):
return v
if isinstance(v, (int, long, float)):
return Date(long(v))
if hasattr(v, 'getTime'):
return Date(v.getTime())
s = text(v)
if s.isdigit():
return Date(long(s))
for value, fmt in ((s.replace('T', ' ')[:19], 'yyyy-MM-dd HH:mm:ss'),
(s.replace('T', ' ')[:16], 'yyyy-MM-dd HH:mm'),
(s, 'EEE MMM dd HH:mm:ss zzz yyyy')):
try:
return system.date.parse(value, fmt)
except:
pass
return None
def assetId(path):
r = rows(system.kanoa.asset.getAssets, {'assetPath': path})
return field(r[0], 'assetId') if r else None
def ownShifts(path):
out = set()
aid = None
for r in rows(system.kanoa.shift.getShifts, {'assetPath': path}):
# only shifts created on this asset, not ones inherited from above
p = norm(field(r, 'assetPath'))
if p:
if p != path:
continue
elif field(r, 'assetId') is not None:
if aid is None:
aid = assetId(path)
if aid is not None and field(r, 'assetId') != aid:
continue
name = text(field(r, 'shiftName'))
if not isPlaceholder(name):
out.add(name)
return out
print '=' * 70
print 'Kanoa Ops Training - Shifts check'
print '=' * 70
# Site time zone, so times are compared the way the shift calendar shows them
siteRows = rows(system.kanoa.asset.getAssets, {'assetPath': SITE})
tzName = text(field(siteRows[0], 'timeZone')) if siteRows else ''
tz = TimeZone.getTimeZone(WINDOWS_TZ.get(tzName.lower(), tzName)) if tzName else TimeZone.getDefault()
print 'Times are compared in %s (%s).' % (tzName or 'this computer\'s time zone', tz.getID())
def local(d):
c = Calendar.getInstance(tz)
c.setTime(d)
return c
def hhmm(c):
return '%02d:%02d' % (c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE))
# 1. Shift names and where each shift is created
print '\n-- Shifts and where they are created'
names = set(text(field(r, 'shiftName')) for r in rows(system.kanoa.shift.getShiftNames, {}))
created = {}
for name, path, days, start, end in SHIFTS:
where = path.split('\\')[-1]
if name not in names:
near = [n for n in names if squash(n) == squash(name)]
if near:
check(False, 'Shift name %s exists' % name,
"Found '%s'. Rename it to '%s' (names are case-sensitive)." % (near[0], name))
else:
check(False, 'Shift name %s exists' % name,
'In Ops > Scheduling > Shifts, select %s, add a shift and type %s as a new shift name.' % (where, name))
continue
if path not in created:
created[path] = ownShifts(path)
own = created[path]
ok = name in own or any(squash(n) == squash(name) for n in own)
check(ok, '%s is created on %s' % (name, where),
'In Ops > Scheduling > Shifts, select %s in the asset selector and add %s there%s.'
% (where, name, (' (found on: %s)' % ', '.join(sorted(own))) if own else ''))
# 2. Lines that should inherit the site shifts
print '\n-- Inheritance'
for path in INHERIT:
own = ownShifts(path)
check(not own, '%s inherits the site shifts' % path.split('\\')[-1],
'It has its own shifts (%s), which stop it inheriting the Philadelphia shifts. Delete them from %s.'
% (', '.join(sorted(own)), path.split('\\')[-1]))
# 3. Each shift's times and how it repeats
# (getShiftCalendarEvents only expands repeating shifts on the gateway, so this reads
# the shift blocks and their recurrence rule instead.)
print '\n-- Shift times and recurrence'
RRULE_DAYS = {'MO': 'Mon', 'TU': 'Tue', 'WE': 'Wed', 'TH': 'Thu', 'FR': 'Fri', 'SA': 'Sat', 'SU': 'Sun'}
NUM_DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] # dateutil: 0 = Monday
def parseRule(rule):
# Returns {'FREQ', 'BYDAY' (list of Mon..Sun), 'INTERVAL', 'UNTIL', 'COUNT'} from either an
# iCalendar string ('RRULE:FREQ=WEEKLY;BYDAY=MO,TU', maybe after a DTSTART line) or JSON
raw = text(rule)
if not raw:
return {}
parts = {}
if raw.startswith('{'):
try:
d = system.util.jsonDecode(raw)
parts = dict((str(k).upper(), d[k]) for k in d)
except:
parts = {}
else:
for line in raw.replace('\r', '\n').split('\n'):
line = line.strip()
if 'FREQ' in line.upper():
line = line.split(':', 1)[-1] if line.upper().startswith('RRULE') else line
for kv in line.split(';'):
if '=' in kv:
parts[kv.split('=', 1)[0].strip().upper()] = kv.split('=', 1)[1].strip()
if not parts:
return {}
days = parts.get('BYDAY', parts.get('BYWEEKDAY'))
if days is None:
days = []
elif isinstance(days, basestring):
days = days.strip('[]()').split(',')
out = []
for d in days:
d = text(d).upper().strip(" '\"")
d = d.split('(')[0].lstrip('+-0123456789') or d # MO(+1), 1MO
if d.isdigit() and int(d) < 7:
out.append(NUM_DAYS[int(d)])
elif d[:2] in RRULE_DAYS:
out.append(RRULE_DAYS[d[:2]])
freq = text(parts.get('FREQ')).upper()
freq = {'0': 'YEARLY', '1': 'MONTHLY', '2': 'WEEKLY', '3': 'DAILY'}.get(freq, freq)
return {'FREQ': freq, 'BYDAY': out, 'INTERVAL': text(parts.get('INTERVAL')) or '1',
'UNTIL': text(parts.get('UNTIL')).replace('-', ''), 'COUNT': text(parts.get('COUNT')), 'RAW': raw}
today = system.date.format(system.date.now(), 'yyyyMMdd')
for name, path, days, start, end in SHIFTS:
where = path.split('\\')[-1]
raw = rows(system.kanoa.shift.getShiftBlocks, {'assetPath': path, 'shiftName': name})
showColumns('getShiftBlocks', raw, 'rruleStr')
# keep only this shift's blocks when the rows say which shift they belong to
blocks = [r for r in raw if 'shiftName' not in r or squash(r['shiftName']) == squash(name)]
if not blocks:
check(False, '%s has a time block on %s' % (name, where),
'No %s on the %s calendar. In Ops > Scheduling > Shifts, select %s and add it.' % (name, where, where))
continue
gotDays, times, repeating, problems, rules = set(), set(), False, [], []
for r in blocks:
s = toDate(field(r, 'startDate', 'start', 'startTime'))
e = toDate(field(r, 'endDate', 'end', 'endTime'))
if s is not None and e is not None:
times.add('%s-%s' % (hhmm(local(s)), hhmm(local(e))))
rule = parseRule(field(r, 'rruleStr', 'rrule'))
if not rule:
if s is not None:
gotDays.add(DAY_NAMES[local(s).get(Calendar.DAY_OF_WEEK)])
continue
repeating = True
freq = rule.get('FREQ', '').upper()
rules.append(rule['RAW'])
if rule['BYDAY']:
gotDays |= set(rule['BYDAY'])
elif freq == 'DAILY':
gotDays |= set(ORDER)
elif s is not None:
gotDays.add(DAY_NAMES[local(s).get(Calendar.DAY_OF_WEEK)])
if rule.get('INTERVAL', '1') not in ('', '1'):
problems.append('it repeats every %s %s' % (rule['INTERVAL'], 'weeks' if freq == 'WEEKLY' else 'periods'))
until = rule.get('UNTIL', '')[:8]
if until and until < today:
problems.append('it stopped repeating on %s-%s-%s' % (until[:4], until[4:6], until[6:8]))
if rule.get('COUNT'):
problems.append('it ends after %s occurrences' % rule['COUNT'])
check(repeating, '%s repeats' % name,
"It's a one-off. Edit it, click 'Does Not Repeat' and set it to repeat weekly on %s." % ', '.join(days))
got = sorted(gotDays, key=ORDER.index)
check(got == days, '%s runs %s' % (name, ', '.join(days)),
"It runs %s. Edit its recurrence: weekly on %s. (Recurrence rule: %s)"
% (', '.join(got) or 'no days', ', '.join(days), ' | '.join(rules) or 'none'))
if problems:
check(False, '%s keeps repeating' % name, 'Edit its recurrence: %s. Set it to never end.' % '; '.join(problems))
check(sorted(times) == ['%s-%s' % (start, end)], '%s is %s to %s' % (name, start, end),
"It's %s. Edit the shift and set it to %s to %s." % (', '.join(sorted(times)) or 'unreadable', start, end))
passed = results.count(True)
print '\n' + '=' * 70
print 'Result: %d of %d checks passed' % (passed, len(results))
if passed == len(results):
print 'All good. Your shifts are set up.'
else:
print 'Fix the FAIL items above, then run this again.'
print '=' * 70
The shift scheduler works like a calendar. You can:
- Create a shift name by typing a new name in the shift name dropdown
- Drag time windows onto the calendar
- Configure recurrence rules, so you don't create each shift instance by hand
- Apply shifts to higher-level assets, such as a site, so the assets below inherit them
- Override inherited shifts at lower-level assets
Because the Day and Evening shifts are created on the site, Machine 1 and Spray Line 1 inherit them. Manual Pack 1 has its own shift, so it no longer inherits the site shifts — once a shift is created at any level, shifts from higher levels are not inherited at that level.


