Skip to main content

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.

Reference Materials: Shifts - Manage personnel shifts across your entire enterprise, whether at the plant level or for specific assets.
Watch Scheduling Shifts Tutorial

Manage personnel shifts across your entire enterprise, whether at the plant level or for specific assets.


Overview​

A shift is a planned operating time window for an asset or group of assets, for example Day Shift, Evening Shift, Night Shift, Weekend Shift, or Manual Pack Shift. Shifts are used for reporting and analysis.

Shifts are independent of production, they don't create, start or stop production runs or store production quantities. They provide time windows that Kanoa uses when analyzing production data, so reports can answer questions such as: How much did we produce on Day Shift? Which shift had the most downtime? What was OEE by shift?

Shifts are stored separately from production events. Kanoa combines production, count, state, and shift data at query time, so the same production events can be analyzed across different time windows without duplicating data.

Shifts are inherited through the asset hierarchy. Typically you configure shifts at the site level, let areas and lines inherit them, and override specific assets only when needed. This reduces repetitive scheduling configuration.

Schedule Shifts


Features​

Site Time Zone
KanoaOPS provides support for enterprises with sites in different timezones. When a site is selected, the calendar will show the shift times in the site's time zone.

Inherited Shifts
Shifts can be created at the site, area, asset level. When an asset doesn't have a shift associated with itself, it will look up the asset hierarchy for a shift. This means that you can create the shifts at the site level and all assets underneath will respect that shift. If an asset or area requires a different shift, simply create the shift at that level. Once a shift is created at any level, shifts from higher levels will no longer be inherited at that level.

Complex Shift Rotation Patterns
We use the dateutil rrule object which offers a complete and fast implementation of the recurrence rules documented in the iCalendar RFC. This makes the scheduling of repeating shifts very simple and pretty much identical to how it is done in Google Calendar, iCalendar and Microsoft's calendar.

To create, say, a four shift rotation pattern, simply create the schedule pattern for each week and set it to repeat every x weeks.

Schedule Operations


Filters​

  • Asset Selector: Click on the asset selector operationsSchedule to select which assets the scheduled events are shown for.
  • Display mode: Scheduled events can be shown on a calendar or as a list.
  • Period: The calendar or list can display events by month, week, or day.

Adding Shifts​

  1. In the calendar view, select the time you wish to schedule the shift for.
  2. Click operationsSchedule button.
  3. Select the asset if it has not already been populated.
  4. Select the shift name. If the shift name does not exist, you can create it by typing the name of it in the dropdown and selecting 'create'.
  5. Click on the circle to choose a color for this shift.
  6. Set start and end times.
  7. For recurring events, click on the 'Does Not Repeat' link to edit recurrence options.

operationsSchedule

Note: Shifts do not need to repeat, but typically should for ongoing operations. User non-repeating shift entries for special one off shifts.


Editing Shifts​

  1. In the calendar view, select the shift you wish to edit and click operationsSchedule button.

Deleting Shifts​

  1. In the calendar view, select the shift you wish to delete and click operationsSchedule button. You can also use Delete key on you keyboard.

From Shifts →

Configure plant shifts

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.
  1. Navigate to Ops → Scheduling → Shifts.
  2. On the Philadelphia site, configure Day Shift, repeating Monday to Friday.
  3. On the Philadelphia site, configure Evening Shift, repeating Monday to Friday.
  4. On Manual Pack 1, configure Manual Pack Shift, repeating Thursday to Friday.
Check your work

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
checkLesson-shifts.py
# 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.