Event-Triggered Compliance Training

Assign training courses based on HOS violations

Overview

This script automates the process of assigning training courses to drivers based on their HOS violations. It interacts with Samsara APIs to fetch drivers based on tags, check existing assignments, and create new assignments for drivers based on their HOS violations.

Note: This guide assumes you are already familiar with the basics of the Samsara Training Product and the overall GET Training Assignments, POST Training Assignments and GET Training Courses APIs. If you are new to Training, see the Driver Training Guide first.

Example Use Cases

  • A Safety manager wants to assign the "Compliance Training" course to a driver when the driver has 2 or more HOS violations in the past 30 days.
  • A Fleet manager wants to assign the "Compliance Training" course to a driver when the driver has 3 HOS violations that are marked as "shiftHour" type in the past 45 days.

Trigger Conditions

  • Specific HOS violation type (e.g., "shiftHours")
  • Specify conditions under which assignments will skip drivers (e.g., skip assignment if the driver has completed or is enrolled in the course within the past 30 days)
  • Specify driver tags for assignment
  • HOS violation count thresholds are exceeded (e.g., 2 violations in 45 days)

Action

  • Assign training course with specific due dates

Execution Flow

  1. Fetch HOS Violations: Convert HOS_VIOLATION_LOOKBACK_START and HOS_VIOLATION_LOOKBACK_END to RFC3339 format. Retrieve HOS violation events that match HOS_VIOLATION_TYPE for the given period and driver tag ID. Count the number of violations for each driver.
  2. Filter Drivers Based on HOS Violation Threshold: Identify drivers who have exceeded the HOS_VIOLATION_THRESHOLD. Flag these drivers for training.
  3. Identify Drivers Who Require Training: Loop through the drivers that meet the above thresholds. If a driver has already been assigned the course, omit those drivers from drivers_requiring_training.
  4. Generate Training Due Date: Call generate_rfc3339_timestamp() with COURSE_COMPLETION_DUE_DAYS to get the training due date in RFC3339 format.
  5. Assign Training: Call assign_training() to assign the course to drivers in drivers_requiring_training with the generated training due date.

Script

Note: To run certain automations, you will need to set up a development environment. Ensure your environment has access to Samsara API endpoints and is properly configured for integration. These code snippets can be copied but will need to be customized to match your organization's specific use case, including correct API tokens and organizational details.

from datetime import datetime, timedelta
import requests

# API URLs and token
HOS_VIOLATION_API = 'https://api.samsara.com/fleet/hos/violations'
ASSIGN_TRAINING_API = 'https://api.samsara.com/training-assignments'
STREAM_TRAINING_ASSIGNMENT_API = 'https://api.samsara.com/training-assignments/stream'
API_TOKEN = 'Insert your API token'
HEADERS = {'Accept': 'application/json', 'Authorization': f'Bearer {API_TOKEN}'}

# Constants
HOS_VIOLATION_LOOKBACK_START = -45  
HOS_VIOLATION_LOOKBACK_END = 0 
TARGET_DRIVER_TAG_IDS = ['Insert the driver tag ID']  
HOS_VIOLATION_TYPE = 'Insert violation type'
HOS_VIOLATION_THRESHOLD = 3
COURSE_ID = 'Insert course ID' 
COURSE_COMPLETION_DUE_DAYS = 14
TRAINING_LOOKBACK_DAYS = -30

def get_hos_violations(start_time, end_time, tag_ids):
    '''
    Fetches Hours of Service (HOS) violations based on a specific time range and tag IDs.

    Args:
        start_time (str): Start time in RFC3339 format.
        end_time (str): End time in RFC3339 format.
        tag_ids (list): List of tag IDs to filter the data.

    Returns:
        list: A list of HOS violations within the specified time range and tags.
    '''
    params = {
        'startTime': start_time,
        'endTime': end_time,
        'tagIds': ','.join(tag_ids),
        'types': HOS_VIOLATION_TYPE,
    }

    hos_violations = []
    after = ''
    has_next_page = True
    while has_next_page:
        params['after'] = after
        response = requests.get(HOS_VIOLATION_API, headers=HEADERS, params=params)
        data = response.json()
        hos_violations.extend(data['data'])
        if data.get('pagination', {}).get('hasNextPage') == True:
            has_next_page = True
            after = data['pagination']['endCursor']
        else:
            has_next_page = False

    return hos_violations

def assign_training(driver_ids, course_id, due_date):
    '''
    Assigns a specific training course to the provided drivers with a due date.

    Args:
        driver_ids (list): List of driver IDs to whom the training should be assigned.
        course_id (str): The ID of the course to be assigned.
        due_date (str): The due date for completing the course in RFC3339 format.

    Returns:
        None
    '''
    params = {'learnerIds': driver_ids, 'courseId': course_id, 'dueAtTime': due_date}

    requests.post(ASSIGN_TRAINING_API, headers=HEADERS, params=params)

def get_training_assignment_details(assignment_interval_timestamp, course_id):
    '''
    Retrieves a stream of filtered training assignments within a specified time range.

    Args:
        assignment_interval_timestamp (str): Start timestamp in RFC3339 format to
        filter the training assignments.
        course_id (str): The ID of the training course to filter the assignments.

    Returns:
        list: A list of training assignments that match the provided filters.
    '''
    training_assignments = []
    after = ''
    params = {'startTime': assignment_interval_timestamp, 'courseIds': [course_id]}
    has_next_page = True
    while has_next_page:
        params['after'] = after
        response = requests.get(
            STREAM_TRAINING_ASSIGNMENT_API, headers=HEADERS, params=params
        )
        data = response.json()
        training_assignments.extend(data['data'])
        if data.get('pagination', {}).get('hasNextPage'):
            has_next_page = True
            after = data['pagination']['endCursor']
        else:
            has_next_page = False

    return training_assignments

def generate_rfc3339_timestamp(delta_days):
    '''
    Generates an RFC3339 timestamp by adding or subtracting days from the current date.

    Args:
        delta_days (int): Number of days to add (positive) or subtract (negative) from the
        current date.

    Returns:
        str: The RFC3339 formatted timestamp.
    '''
    target_date = (datetime.now() + timedelta(days=delta_days)).replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    return target_date.strftime('%Y-%m-%dT%H:%M:%SZ')

def generate_milliseconds_timestamp(delta_days):
    '''
    Generates a timestamp in milliseconds format by adding or subtracting
    days from the current date.

    Args:
        delta_days (int): Number of days to add (positive) or subtract(negative)
        from the current date.

    Returns:
        int: The timestamp in milliseconds.
    '''
    target_date = (datetime.now() + timedelta(days=delta_days)).replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    return int(target_date.timestamp()) * 1000

def main():
    # Generate RFC3339 timestamps for the HOS violation lookback start and end times
    start_time_rfc3339 = generate_rfc3339_timestamp(HOS_VIOLATION_LOOKBACK_START)
    end_time_rfc3339 = generate_rfc3339_timestamp(HOS_VIOLATION_LOOKBACK_END)

    # Fetch HOS violation events for the specified lookback period and target drivers
    hos_violation_events = get_hos_violations(
        start_time_rfc3339, end_time_rfc3339, TARGET_DRIVER_TAG_IDS
    )

    # Track the count of HOS violations per driver
    driver_hos_violation_count = {}
    for event in hos_violation_events:
        violations = event['violations']
        for violation in violations:
            driver_id = violation['driver']['id']
            if driver_id not in driver_hos_violation_count:
                driver_hos_violation_count[driver_id] = 1
            else:
                driver_hos_violation_count[driver_id] += 1

    # Flag drivers who exceed the HOS violation threshold
    flagged_drivers = {
        driver_id
        for driver_id, count in driver_hos_violation_count.items()
        if count >= HOS_VIOLATION_THRESHOLD
    }

    # Fetch the details of drivers who have already been assigned training
    drivers_assigned_training = []
    training_lookback_days_rfc3339 = generate_rfc3339_timestamp(TRAINING_LOOKBACK_DAYS)
    assignment_details = get_training_assignment_details(
        training_lookback_days_rfc3339, COURSE_ID
    )
    for assignment in assignment_details:
        if assignment['learner']['type'] == 'driver':
            drivers_assigned_training.append(assignment['learner']['id'])

    # Identify drivers who are flagged for violations and haven't been assigned training
    drivers_requiring_training = [
        driver for driver in flagged_drivers if driver not in drivers_assigned_training
    ]

    drivers_requiring_training = ','.join(
        [f'driver-{id}' for id in drivers_requiring_training]
    )

    # Generate RFC3339 timestamp for the course completion due date (14 days from now)
    course_due_date_rfc3339 = generate_rfc3339_timestamp(COURSE_COMPLETION_DUE_DAYS)

    # Assign the training to the filtered drivers requiring it
    assign_training(drivers_requiring_training, COURSE_ID, course_due_date_rfc3339)

if __name__ == '__main__':
    main()

Appendix

Constant Definitions

ConstantTypeDescription
HOS_VIOLATION_LOOKBACK_STARTIntegerDefines how many days back to look for HOS violation events.
HOS_VIOLATION_LOOKBACK_ENDIntegerDefines the end date for looking up HOS violation events.
TARGET_DRIVER_TAG_IDSList of StringsList of tag IDs used to filter specific drivers.
HOS_VIOLATION_TYPEStringDefine the specific HOS violation type to track.
HOS_VIOLATION_THRESHOLDIntegerA threshold used to filter drivers based on the count of HOS violation. Only drivers exhibiting the behavior at or above this threshold are considered for training.
COURSE_IDStringID of the training course that will be assigned.
COURSE_COMPLETION_DUE_DAYSIntegerNumber of days from today to set as the due date for training assignments.
TRAINING_LOOKBACK_DAYSIntegerNumber of days to look back for training assignments.

Function Definitions

FunctionDescription
get_hos_violationsFetches the HOS violation events for a given time range, tag IDs and violation type. It retrieves all available pages of data using pagination.
assign_trainingThis function is responsible for creating training assignments for drivers. It accepts three parameters: course_id, which is the ID of the training course to be assigned, driver_ids, which is a list of driver IDs to receive the training, and due_date, which is the due date for completing the training.
get_training_assignment_detailsThis function retrieves a stream of training assignments filtered by a specified assignment_interval_timestamp and course_id. It handles pagination to fetch all relevant assignment data from the Samsara API. If successful, it returns a list of assignment records.
generate_rfc3339_timestampThis function generates a timestamp in milliseconds format by either adding or subtracting a specified number of days (given by delta_days) from the current date. To calculate a date in the past, pass a negative value for delta_days; for a future date, pass a positive value. This allows the function to determine whether the days should be added or subtracted. The timestamp generated in this example is midnight and in the UTC timezone. Please refer to Samsara Timestamp Documentation for more information on this.
generate_milliseconds_timestampThis function generates a timestamp in milliseconds format by either adding or subtracting a specified number of days (given by delta_days) from the current date. To calculate a date in the past, pass a negative value for delta_days; for a future date, pass a positive value. This allows the function to determine whether the days should be added or subtracted..