Skip to main content

OLE / POLE Labor Effectiveness Analysis

OLE and POLE extend the standard OEE (Availability × Performance × Quality) model with a labor dimension - they answer "how effective was the operator," not just "how effective was the asset."

OLE Dashboard

Both share the same Performance and Quality factors as OEE. They differ only in how Availability is defined:

MetricAvailability numeratorAnswers
OLE (Operator Labor Effectiveness)loggedInSecs - raw presence"Was the operator logged in, regardless of whether the asset was running?"
POLE (Productive OLE)productiveSecs - logged in and the asset was actually running"Was the operator present and was work actually happening?"
oleAvailability = loggedInSecs / scheduledSecs
poleAvailability = productiveSecs / scheduledSecs
performance = rate achieved vs. standard rate, while productive
quality = yield after waste

ole = oleAvailability * performance * quality
pole = poleAvailability * performance * quality

The gap between oleAvailability and poleAvailability for the same operator/period is itself meaningful: it's time the operator was present but the machine wasn't producing - changeover, waiting on materials, unassigned downtime. This is exposed on the dashboard as the "Labor Gap" KPI tile.

Crew attribution

When more than one operator is logged into the same asset at once, counter output (infeed/outfeed/waste) is split between them via attribution mode, so credit isn't double-counted:

  • EvenSplit (default) - time-weighted share based on how many other operators were concurrently logged in.
  • FullCredit - every concurrent operator gets full credit (e.g. training/shadowing).
  • PrimaryOnly - only the first-logged-in operator at each moment gets performance/quality credit; others still get their own OLE/POLE availability from their own session.

Data Model Dependencies

Table/ViewRole
mes.operatorAssetSessionLogin/logout events - the source of loggedInSecs
sch.operatorShiftAssignment / sch.shiftBlockLabor roster - the source of scheduledSecs for scheduled operators
mes.stateEvent / mes.state / mes.stateTypeAsset run-state history - the source of productiveSecs (via the Running state type)
mes.counter / mes.counterEventInfeed/outfeed/waste counts
mes.itemAssetRateStandard/schedule rate configuration, one row per (itemId, assetId)
mes.itemPeriodLookup for the rate's period basis (e.g. Hour, Batch (in mins))
mes.vwAssetItemJoins itemAssetRate through to itemPeriod with clearer enabled-flag names
mes.modeEventRecords which itemId was running on an asset over time; can also carry a standardRate override
Standard rate resolution

standardRate is not read directly off itemAssetRate. It's resolved via mes.fnGetStandardRateAndPeriod, which finds the most recent modeEvent for the asset (which carries the currently-running itemId), joins that to itemAssetRate for the baseline rate, and lets modeEvent.standardRate override it when explicitly set. If this resolves to NULL (no modeEvent row for the asset), performance - and therefore ole/pole - will correctly come back as 0, not an error.

No date range extends into the future

Every OLE/POLE proc clamps both @startDate and @endDate to GETDATE() if either is set later than "now" - mirroring the existing convention in mes.fnGetRunTimesAndCounts. Time that hasn't happened yet is never counted as scheduled/attendable time.

Table Functions

mes.fnGetOperatorSessionSeconds(@userId INT, @assetId INT, @rangeStart DATETIME, @rangeEnd DATETIME) RETURNS INT

Total seconds this user was logged into @assetId (or any asset, if @assetId is NULL) within the range. The OLE availability numerator - raw presence, independent of whether the asset was running.

mes.fnGetOperatorProductiveSeconds(@userId INT, @assetId INT, @rangeStart DATETIME, @rangeEnd DATETIME, @runStateTypeId INT) RETURNS INT

Seconds where this user was both logged into @assetId and the asset's state type was @runStateTypeId (normally Running). The POLE availability numerator.

mes.fnGetOperatorAttributionShare(@userId INT, @assetId INT, @rangeStart DATETIME, @rangeEnd DATETIME, @attributionMode VARCHAR(20)) RETURNS FLOAT

Time-weighted share (0-1) of @assetId's counter output attributable to @userId over the range, given how many other users were concurrently logged into the same asset. See @attributionMode values above.

mes.fnGetStandardRateAndPeriod(@assetId INT, @asOf DATETIME)
RETURNS TABLE (standardRate FLOAT, period VARCHAR(25))

Resolves the standard rate and its period basis for an asset as of a point in time: finds the asset's most recent modeEvent at/before @asOf, joins to mes.vwAssetItem on (assetId, itemId) for the baseline standardRate/itemPeriodName, and lets the modeEvent's own standardRate override the baseline when set.

mes.fnGetOLEMetrics(@scheduledSecs INT, @loggedInSecs INT, @productiveSecs INT, @infeedCount FLOAT, @outfeedCount FLOAT, @wasteCount FLOAT, @packageCount FLOAT,@standardRate FLOAT, @period VARCHAR(50)) RETURNS TABLE

Mirrors mes.fnGetOEEMetrics's shape - the shared function every OLE/POLE proc calls to turn raw seconds/counts into oleAvailability, poleAvailability, performance, and quality. Callers multiply these together for the final ole/pole scores.


Stored Procedures

mes.spGetOLEData(@userId INT, @assetId INT, @startDate DATETIME, @endDate DATETIME, @attributeMode VARCHAR(20)))

Single-range OLE/POLE for one operator on one asset - or every operator in the "crew" (scheduled or logged into the asset during the range) when @userId is NULL. attributionMode defaults to 'EvenSplit' if null Setting userId to null returns one row per crew operator

mes.spGetOLEDataByInterval(@userId INT, @assetId INT, @startDate DATETIME, @endDate DATETIME, @interval VARCHAR(10), @attributeMode VARCHAR(20)))

Hour/Day/Week bucketed version of spGetOLEData. Adds @interval VARCHAR(10) ('Hour' / 'Day' / 'Week' / NULL for auto-sized buckets based on range length). Note: @interval controls bucketing granularity only - it is not the same thing as the rate's @period basis (which still comes from fnGetStandardRateAndPeriod per-bucket).

mes.spGetOLEDataByShift(@userId INT, @assetId INT, @startDate DATETIME, @endDate DATETIME, @attributeMode VARCHAR(20)))

One row per sch.operatorShiftAssignment (+shiftBlock) overlapping the range - a rostered shift instance is already a natural bucket. Adds @assetId INT = NULL; required when @userId is NULL (there's no "every user's every shift" mode). Unlike the other procs, there's no login-session union here - a shift bucket only exists where there's a schedule row, so an off-schedule login has no shift-shaped bucket to appear under.

mes.spGetOLEDataAsset(@assetId INT, @startDate DATETIME, @endDate DATETIME))

Single-range, whole-crew-combined OLE/POLE for one asset - no operator dimension at all. productiveSecs/counts come from mes.fnGetRunTimesAndCounts (the asset's own run-state, immune to crew-size double-counting); loggedInSecs is the sum of all login time on the asset; scheduledSecs is the requested range's own wall-clock duration. Backs the dashboard's KPI scorecard tiles.

mes.spGetOLEDataAssetByInterval(@userId INT, @assetId INT, @startDate DATETIME, @endDate DATETIME, @interval VARCHAR(10)))

Bucketed version of spGetOLEDataAsset. Backs the dashboard's trend chart.

mes.spGetOLEOperatorLeaderboard @assetId INT, @startDate DATETIME, @endDate DATETIME, @attributionMode VARCHAR(20))

One row per operator, ranked by POLE descending, aggregated across all of that operator's own shifts (safe to sum - same person, non-overlapping shifts).

Unscheduled fallback

Operators with zero shift assignments for the asset/range but who did log into it get a single fallback bucket spanning the entire requested range, instead of being silently excluded. A user with any real shift assignment is not blended with this fallback - their login time outside scheduled buckets simply isn't counted, to avoid double-counting.

This means "Attendance %" means different things depending on the row: for scheduled operators it's "% of your assigned shift you were logged in"; for unscheduled operators it's "% of the entire requested window you were logged in" - a legitimate but broader question. Low attendance numbers for someone who only recently logged in during a multi-day analysis window are expected, not a bug.

MAX(standardRate)/MAX(period) across an operator's shifts is an approximation if the asset's rate changed mid-range - acceptable for a ranked leaderboard, not for a single precise number.

Python scripting API

All six wrappers live under system.kanoa.operators.

def getOLEData(userId, assetId, startDate, endDate, attributionMode)
def getOLEDataByInterval(userId, assetId, startDate, endDate, interval, attributionMode)
def getOLEDataByShift(userId, startDate, endDate, assetId, attributionMode)
def getOLEDataAsset(assetId, startDate, endDate)
def getOLEDataAssetByInterval(assetId, startDate, endDate, interval)
def getOLEOperatorLeaderboard(assetId, startDate, endDate, attributionMode)

Perspective Components

All under kanoa/mes/operator/, composed into the kanoa/core/dbp/screens/analytics/oleAnalysisDashboard screen.

ViewRole
oleAnalysisDashboardScreen shell - asset/date/interval/attribution-mode selectors, composes the four widgets below
oleKpiTileOne reusable scorecard tile (label/value/sub/badge), repeated 6× for Attendance, OLE, POLE, Performance, Quality, and Labor Gap
oleTrendChartOLE vs. POLE availability over time, via embr.chart.apex-charts, timestamp-labeled per the selected interval
oleUtilizationBreakdownTwo stacked bars: Asset Time (Productive / Unplanned / Planned downtime, from fnGetRunTimesAndCounts) and Labor Time (Logged In / Absent) - kept as two independent bars because productiveSecs (asset runtime) and loggedInSecs (labor presence) are independent numbers and don't sum to one meaningful total
oleOperatorLeaderboardHeader + a repeated oleLeaderboardRow per operator, ranked by POLE
oleLeaderboardRowOne leaderboard row: rank, name, Attendance/OLE/POLE as color-coded progress bars, plain Performance/Quality percentages

Known limitations

  • Standard rate must be configured. If mes.modeEvent has no row for an asset, standardRate resolves to NULL, forcing performance (and therefore ole/pole) to 0 even when availability is good. This is correct behavior, not a bug - it surfaces a missing rate configuration rather than fabricating a number.
  • Scheduling is optional per asset. Assets/operators with no sch.operatorShiftAssignment data fall back to session-derived attendance (see "Unscheduled fallback" above) rather than returning empty results.
  • Overlapping session data will double-count. fnGetOperatorSessionSeconds sums every qualifying session row without merging overlapping intervals - two overlapping (rather than sequential) login records for the same user/asset will inflate loggedInSecs past the wall-clock window. This is a data-quality assumption (session records shouldn't overlap), not something the function defends against.