[VOL-3703] Performance Monitoring Attributes test
Currently test suite contains three test cases:
1. Test of pm data with default values
2. Test of pm data with user values
3. Test of pm data for disabled devices
Test 1 runs about 35 minutes.
Test 2 runs about  2 minutes.
Test 3 runs about  4 minutes

The suite is designed in a generic way. It collects all values to check by itself.
Further validation data can be passed via yaml file like ../data/pm-data.yaml
To have a determined state of devices a Sanity Check will be executed first.

Actually commands 'voltctl device pmconfig group list ...' printout the intvals without time unit.
Therefore some workarounds still included in this patch!

Change-Id: If115e85471221c321e8764cc890af583090189b2
diff --git a/Makefile b/Makefile
index 68773e4..9823039 100755
--- a/Makefile
+++ b/Makefile
@@ -50,6 +50,7 @@
 ROBOT_DMI_SINGLE_BBSIM_FILE     ?= $(ROOT_DIR)/tests/data/dmi-components-bbsim.yaml
 ROBOT_DMI_SINGLE_ADTRAN_FILE     ?= $(ROOT_DIR)/tests/data/dmi-components-adtran.yaml
 ROBOT_SW_UPGRADE_FILE     ?= $(ROOT_DIR)/tests/data/software-upgrade.yaml
+ROBOT_PM_DATA_FILE     ?= $(ROOT_DIR)/tests/data/pm-data.yaml
 
 # for backwards compatibility
 sanity-kind: sanity-single-kind
@@ -335,6 +336,46 @@
 	cd tests/dmi-interface ;\
 	robot -V $(ROBOT_CONFIG_FILE) $(ROBOT_MISC_ARGS) $(ROBOT_FILE)
 
+# target to invoke single ONU pm data scenarios in ATT workflow
+voltha-pm-data-single-kind-att: ROBOT_MISC_ARGS += -v workflow:ATT
+voltha-pm-data-single-kind-att: ROBOT_CONFIG_FILE := $(ROBOT_SANITY_SINGLE_PON_FILE)
+voltha-pm-data-single-kind-att: voltha-pm-data-tests
+
+# target to invoke single ONU pm data scenarios in DT workflow
+voltha-pm-data-single-kind-dt: ROBOT_MISC_ARGS += -v workflow:DT
+voltha-pm-data-single-kind-dt: ROBOT_CONFIG_FILE := $(ROBOT_SANITY_DT_SINGLE_PON_FILE)
+voltha-pm-data-single-kind-dt: voltha-pm-data-tests
+
+# target to invoke single ONU pm data scenarios in TT workflow
+voltha-pm-data-single-tt: ROBOT_MISC_ARGS += -v workflow:TT
+voltha-pm-data-single-tt: ROBOT_CONFIG_FILE := $(ROBOT_SANITY_TT_SINGLE_PON_FILE)
+voltha-pm-data-single-tt: voltha-pm-data-tests
+
+# target to invoke multiple OLTs pm data scenarios in ATT workflow
+voltha-pm-data-multiolt-kind-att: ROBOT_MISC_ARGS += -v workflow:ATT
+voltha-pm-data-multiolt-kind-att: ROBOT_CONFIG_FILE := $(ROBOT_SANITY_MULTIPLE_OLT_FILE)
+voltha-pm-data-multiolt-kind-att: voltha-pm-data-tests
+
+# target to invoke multiple OLTs pm data scenarios in DT workflow
+voltha-pm-data-multiolt-kind-dt: ROBOT_MISC_ARGS += -v workflow:DT
+voltha-pm-data-multiolt-kind-dt: ROBOT_CONFIG_FILE := $(ROBOT_SANITY_DT_MULTIPLE_OLT_FILE)
+voltha-pm-data-multiolt-kind-dt: voltha-pm-data-tests
+
+# target to invoke multiple OLTs pm data scenarios in TT workflow
+voltha-pm-data-multiolt-kind-tt: ROBOT_MISC_ARGS += -v workflow:TT
+voltha-pm-data-multiolt-kind-tt: ROBOT_CONFIG_FILE := $(ROBOT_SANITY_TT_MULTIPLE_OLT_FILE)
+voltha-pm-data-multiolt-kind-tt: voltha-pm-data-tests
+
+voltha-pm-data-tests: ROBOT_MISC_ARGS += -i functional -e PowerSwitch $(ROBOT_DEBUG_LOG_OPT)
+voltha-pm-data-tests: ROBOT_PM_CONFIG_FILE := $(ROBOT_PM_DATA_FILE)
+voltha-pm-data-tests: ROBOT_FILE := Voltha_ONUPMTests.robot
+voltha-pm-data-tests: voltha-pm-data-test
+
+voltha-pm-data-test: vst_venv
+	source ./$</bin/activate ; set -u ;\
+	cd tests/pm-data ;\
+	robot -V $(ROBOT_CONFIG_FILE) -V $(ROBOT_PM_CONFIG_FILE) $(ROBOT_MISC_ARGS) $(ROBOT_FILE)
+
 # ONOS Apps to test for Software Upgrade need to be passed in the 'onos_apps_under_test' variable in format:
 # <app-name>,<version>,<oar-url>*<app-name>,<version>,<oar-url>*
 onos-app-upgrade-test: ROBOT_MISC_ARGS +=  -e notready -i functional
diff --git a/libraries/onu_utilities.robot b/libraries/onu_utilities.robot
index 478bc56..dba9bc9 100755
--- a/libraries/onu_utilities.robot
+++ b/libraries/onu_utilities.robot
@@ -386,14 +386,17 @@
 
 Wait for Ports in ONOS for all OLTs
     [Documentation]    Waits untill a certain number of ports are enabled in all OLTs
-    [Arguments]    ${onos_ssh_connection}    ${count}    ${filter}    ${max_wait_time}=10m
+    [Arguments]    ${onos_ssh_connection}    ${count}    ${filter}    ${max_wait_time}=10m   ${determine_number}=False
     FOR    ${J}    IN RANGE    0    ${num_olts}
         ${olt_serial_number}=    Set Variable    ${list_olts}[${J}][sn]
         ${onu_count}=    Set Variable    ${list_olts}[${J}][onucount]
         ${of_id}=    Wait Until Keyword Succeeds    ${timeout}    15s    Validate OLT Device in ONOS
         ...    ${olt_serial_number}
         Set Global Variable    ${of_id}
-        ${count2check}    Set Variable If    ${count}==${num_all_onus}    ${onu_count}    ${count}
+        ${count2check}=    Set Variable If    ${count}==${num_all_onus}    ${onu_count}    ${count}
+        # if flag determine_number is set to True, always determine the number of real ONUs (overwrite previous value)
+        ${count2check}=    Run Keyword If    ${determine_number}    Determine Number Of ONU    ${olt_serial_number}
+        ...                ELSE              Set Variable    ${count2check}
         Wait for Ports in ONOS    ${onos_ssh_connection}    ${count2check}    ${of_id}    BBSM    ${max_wait_time}
     END
 
diff --git a/libraries/pm_utilities.robot b/libraries/pm_utilities.robot
new file mode 100755
index 0000000..2fad74d
--- /dev/null
+++ b/libraries/pm_utilities.robot
@@ -0,0 +1,673 @@
+# Copyright 2021 - present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+*** Settings ***
+Documentation     Library for various pm data (metrics) test utilities
+Library           String
+Library           DateTime
+Library           Process
+Library           Collections
+Library           RequestsLibrary
+Library           OperatingSystem
+Library           CORDRobot
+Library           ImportResource    resources=CORDRobot
+Library           utility.py    WITH NAME    utility
+Resource          ./voltctl.robot
+
+*** Variables ***
+# operators for value validations, needed for a better reading only
+${gt}      >      # greater than
+${ge}      >=     # greater equal
+${lt}      <      # less than
+${le}      <=     # less equal
+${eq}      ==     # equal
+${ne}      !=     # not equal
+
+*** Keywords ***
+#################################################################
+# pre test keywords
+#################################################################
+Create Metric Dictionary
+    [Documentation]    Create metric dict of metrics to test/validate
+    ...                Created dictionary has to be set to suite variable!!!
+    ${metric_dict}=     Create Dictionary
+    FOR    ${I}    IN RANGE    0    ${num_all_onus}
+        ${src}=    Set Variable    ${hosts.src[${I}]}
+        ${onu_device_id}=    Get Device ID From SN    ${src['onu']}
+        Continue For Loop If    '${onu_device_id}' in ${metric_dict}
+        # read available metric groups
+        ${group_list}    ${interval_dict}=    Read Group List   ${onu_device_id}
+        # read available groupmetrics
+        ${groupmetrics_dict}=    Get Available Groupmetrics per Device    ${group_list}    ${onu_device_id}
+        ${device_dict}=    Create Dictionary    MetricData    ${groupmetrics_dict}    GroupList    ${group_list}
+        Set To Dictionary    ${metric_dict}    ${onu_device_id}    ${device_dict}
+        Prepare Interval and Validation Data    ${metric_dict}    ${onu_device_id}    ${interval_dict}
+    END
+    log    ${metric_dict}
+    [return]    ${metric_dict}
+
+Get Available Groupmetrics per Device
+    [Documentation]    Delivers avaiable groupmetrics incl. validation data of onu
+    [Arguments]    ${group_list}    ${onu_device_id}
+    ${groupmetric_dict}    Create Dictionary
+    FOR    ${Item}    IN    @{group_list}
+        ${item_dict}=    Read Group Metric Dict   ${onu_device_id}    ${Item}
+        ${item_dict}=    Set Validation Operation    ${item_dict}    ${Item}
+        ${item_dict}=    Set User Validation Operation    ${item_dict}    ${Item}
+        ${item_dict}=    Set User Precondition Operation for Availability    ${item_dict}    ${Item}
+        ${metric}=       Create Dictionary    GroupMetrics=${item_dict}
+        ${dict}=       Create Dictionary    ${Item}=${metric}
+        Set To Dictionary    ${groupmetric_dict}    ${Item}=${metric}
+    END
+    [return]    ${groupmetric_dict}
+
+Set Validation Operation
+    [Documentation]    Sets the validation operation per metric parameter
+    [Arguments]    ${item_dict}    ${group}
+    FOR    ${item}    IN    @{item_dict.keys()}
+        ${type}=    Get From Dictionary    ${item_dict["${item}"]}    type
+        ${item_dict}=     Run Keyword If
+        ...               '${type}'=='COUNTER'    Set Validation Operation For Counter    ${item_dict}    ${item}    ${group}
+        ...    ELSE IF    '${type}'=='CONTEXT'    Set Validation Operation For Context    ${item_dict}    ${item}    ${group}
+        ...    ELSE IF    '${type}'=='GAUGE'      Set Validation Operation For Gauge      ${item_dict}    ${item}    ${group}
+        ...    ELSE       Run Keyword And Continue On Failure    FAIL    Type (${type}) is unknown!
+    END
+    [return]    ${item_dict}
+
+Set Validation Operation For Counter
+    [Documentation]    Sets the validation operation for a counter
+    [Arguments]    ${item_dict}    ${metric}    ${group}
+    # will be overwritten by user operation and value if available
+    ${first}=    Create Dictionary    operator=${ge}    operand=0
+    # for real POD it must be >= previous counter value, in case of BBSim usage we got random values, so check >= 0
+    ${successor}=     Run Keyword If    ${has_dataplane}    Create Dictionary    operator=${ge}    operand=previous
+    ...    ELSE    Create Dictionary    operator=${ge}    operand=0
+    ${ValidationOperation}=    Create Dictionary    first=${first}    successor=${successor}
+    Set To Dictionary    ${item_dict['${metric}']}  ValidationOperation=${ValidationOperation}
+    [return]    ${item_dict}
+
+Set Validation Operation For Context
+    [Documentation]    Sets the validation operation for a context
+    [Arguments]    ${item_dict}    ${metric}    ${group}
+    # will be overwritten by user operation and value if available
+    ${first}=    Create Dictionary    operator=${ge}    operand=0
+    ${successor}=     Create Dictionary    operator=${eq}    operand=previous
+    ${ValidationOperation}=    Create Dictionary    first=${first}    successor=${successor}
+    Set To Dictionary    ${item_dict['${metric}']}  ValidationOperation=${ValidationOperation}
+    [return]    ${item_dict}
+
+Set Validation Operation For Gauge
+    [Documentation]    Sets the validation operation for a gauge
+    [Arguments]    ${item_dict}    ${metric}    ${group}
+    # will be overwritten by user operation and value if available
+    ${first}=    Create Dictionary    operator=${ge}    operand=0
+    ${successor}=     Create Dictionary    operator=${ge}    operand=0
+    ${ValidationOperation}=    Create Dictionary    first=${first}    successor=${successor}
+    Set To Dictionary    ${item_dict['${metric}']}  ValidationOperation=${ValidationOperation}
+    [return]    ${item_dict}
+
+Set User Validation Operation
+    [Documentation]    Sets the user validation operation and value per metric parameter if available
+    [Arguments]    ${item_dict}    ${group}
+    ${variables}=  Get variables    no_decoration=Yes
+    Return From Keyword If    not "pm_user_validation_data" in $variables      ${item_dict}
+    Return From Keyword If    not '${group}' in ${pm_user_validation_data}      ${item_dict}
+    FOR    ${item}    IN    @{item_dict.keys()}
+        Continue For Loop If    not '${item}' in ${pm_user_validation_data['${group}']}
+        ${operator}=    Get From Dictionary    ${pm_user_validation_data['${group}']['${item}']}    firstoperator
+        ${operand}=     Get From Dictionary    ${pm_user_validation_data['${group}']['${item}']}    firstvalue
+        ${first}=    Create Dictionary    operator=${operator}    operand=${operand}
+        ${operator}=    Get From Dictionary    ${pm_user_validation_data['${group}']['${item}']}    successoroperator
+        ${operand}=     Get From Dictionary    ${pm_user_validation_data['${group}']['${item}']}    successorvalue
+        ${successor}=    Create Dictionary    operator=${operator}    operand=${operand}
+        ${ValidationOperation}=    Create Dictionary    first=${first}    successor=${successor}
+        Set To Dictionary    ${item_dict['${item}']}  ValidationOperation=${ValidationOperation}
+    END
+    [return]    ${item_dict}
+
+Set User Precondition Operation for Availability
+    [Documentation]    Sets the user precondition operation, value and element per metric parameter if available
+    [Arguments]    ${item_dict}    ${group}
+    ${variables}=  Get variables    no_decoration=Yes
+    Return From Keyword If    not "pm_user_precondition_data" in $variables      ${item_dict}
+    Return From Keyword If    not '${group}' in ${pm_user_precondition_data}      ${item_dict}
+    FOR    ${item}    IN    @{item_dict.keys()}
+        Continue For Loop If    not '${item}' in ${pm_user_precondition_data['${group}']}
+        ${operator}=    Get From Dictionary    ${pm_user_precondition_data['${group}']['${item}']}    operator
+        ${operand}=     Get From Dictionary    ${pm_user_precondition_data['${group}']['${item}']}    value
+        ${element}=     Get From Dictionary    ${pm_user_precondition_data['${group}']['${item}']}    precondelement
+        ${precond}=     Create Dictionary    operator=${operator}    operand=${operand}    element=${element}
+        Set To Dictionary    ${item_dict['${item}']}  Precondition=${precond}
+    END
+    [return]    ${item_dict}
+
+Prepare Interval and Validation Data
+    [Documentation]    Prepares interval and validation data of onu
+    [Arguments]    ${METRIC_DICT}    ${onu_device_id}    ${interval_dict}
+    ${list}=    Get From Dictionary     ${METRIC_DICT['${onu_device_id}']}    GroupList
+    FOR    ${Item}    IN    @{list}
+        ${metric}=    Get From Dictionary     ${METRIC_DICT['${onu_device_id}']['MetricData']}    ${Item}
+        Set To Dictionary    ${metric}    NumberOfChecks=0
+        ${default_interval}=    Get From Dictionary    ${interval_dict}    ${Item}
+        # convert interval in seconds and remove unit
+        ${default_interval}=    Validate Time Unit    ${default_interval}    False
+        ${intervals}    Create Dictionary    default=${default_interval}    user=-1    current=${default_interval}
+        Set To Dictionary    ${metric}    Intervals=${intervals}
+        Set To Dictionary    ${METRIC_DICT['${onu_device_id}']['MetricData']}    ${Item}=${metric}
+    END
+
+Prepare Group Interval List per Device Id
+    [Documentation]    Prepares group-interval list per device id
+    [Arguments]    ${dev_id}   ${user}=False    ${group}=${EMPTY}
+    ${list}=    Get From Dictionary    ${METRIC_DICT['${dev_id}']}    GroupList
+    ${group_interval_list}   Create List
+    FOR    ${Item}    IN    @{list}
+        ${val}=    Run Keyword If    ${user}
+        ...                Get From Dictionary    ${METRIC_DICT['${dev_id}']['MetricData']['${Item}']['Intervals']}    user
+        ...        ELSE    Get From Dictionary    ${METRIC_DICT['${dev_id}']['MetricData']['${Item}']['Intervals']}    default
+        ${dict}=     Create Dictionary    group=${Item}    interval=${val}
+        Run Keyword If    '${group}'=='${EMPTY}' or '${group}'=='${Item}'   Append To List    ${group_interval_list}    ${dict}
+    END
+    [return]   ${group_interval_list}
+
+Get Longest Interval per Onu
+    [Documentation]    Delivers longest group interval per device id
+    [Arguments]    ${dev_id}   ${user}=False
+    ${list}=    Get From Dictionary    ${METRIC_DICT['${dev_id}']}    GroupList
+    ${longest_interval}=    Set Variable    0
+    FOR    ${Item}    IN    @{list}
+        ${val}=    Run Keyword If    ${user}
+        ...                Get From Dictionary    ${METRIC_DICT['${dev_id}']['MetricData']['${Item}']['Intervals']}    user
+        ...        ELSE    Get From Dictionary    ${METRIC_DICT['${dev_id}']['MetricData']['${Item}']['Intervals']}    default
+        ${longest_interval}=    Set Variable If    ${val} > ${longest_interval}    ${val}    ${longest_interval}
+    END
+    [return]   ${longest_interval}
+
+Get Longest Interval
+    [Documentation]    Delivers longest interval over all devices
+    [Arguments]    ${user}=False
+    ${longest_interval}=    Set Variable    0
+    ${onu_list}    Create List
+    FOR    ${INDEX}    IN RANGE    0    ${num_all_onus}
+        ${onu_device_id}=    Get Device ID From SN    ${hosts.src[${INDEX}].onu}
+        ${onu_id}=    Get Index From List    ${onu_list}   ${onu_device_id}
+        Continue For Loop If    -1 != ${onu_id}
+        Continue For Loop If    not '${onu_device_id}' in ${METRIC_DICT}
+        ${onu_longest_interval}=    Run Keyword If     '${onu_device_id}'!='${EMPTY}'
+        ...    Get Longest Interval per Onu    ${onu_device_id}    ${user}
+        ${longest_interval}=    Set Variable If    ${onu_longest_interval} > ${longest_interval}    ${onu_longest_interval}
+        ...    ${longest_interval}
+    END
+    [return]    ${longest_interval}
+
+Determine Collection Interval
+    [Documentation]    Delivers collection interval over all devices
+    [Arguments]    ${user}=False
+    ${longest_interval}=    Get Longest Interval    user=${user}
+    ${collect_interval}=    evaluate    ((${longest_interval}*2)+(${longest_interval}*0.2))
+    ${collect_interval}=    Validate Time Unit    ${collect_interval}
+    [return]    ${collect_interval}
+
+Set Group Interval per Onu
+    [Documentation]    Sets group user interval in METRIC_DICT per device id
+    [Arguments]    ${device_id}     ${group}    ${val}
+    # convert interval in seconds and remove unit
+    ${val}=    Validate Time Unit    ${val}    False
+    Run Keyword If    '${group}' in ${METRIC_DICT['${device_id}']['MetricData']}
+    ...    Set To Dictionary    ${METRIC_DICT['${device_id}']['MetricData']['${group}']['Intervals']}    user=${val}
+
+Set Group Interval All Onu
+    [Documentation]    Sets group user interval in METRIC_DICT
+    [Arguments]    ${group}    ${val}
+    ${onu_list}    Create List
+    FOR    ${INDEX}    IN RANGE    0    ${num_all_onus}
+        ${onu_device_id}=    Get Device ID From SN    ${hosts.src[${INDEX}].onu}
+        ${onu_id}=    Get Index From List    ${onu_list}   ${onu_device_id}
+        Continue For Loop If    -1 != ${onu_id}
+        Continue For Loop If    not '${onu_device_id}' in ${METRIC_DICT}
+        Set Group Interval per Onu    ${onu_device_id}    ${group}    ${val}
+    END
+
+Activate And Validate Interval All Onu
+    [Documentation]    Activates and validates group user interval taken from METRIC_DICT
+    [Arguments]    ${user}=False    ${group}=${EMPTY}
+    ${onu_list}    Create List
+    FOR    ${INDEX}    IN RANGE    0    ${num_all_onus}
+        ${onu_device_id}=    Get Device ID From SN    ${hosts.src[${INDEX}].onu}
+        ${onu_id}=    Get Index From List    ${onu_list}   ${onu_device_id}
+        Continue For Loop If    -1 != ${onu_id}
+        Continue For Loop If    not '${onu_device_id}' in ${METRIC_DICT}
+        ${list}=    Prepare Group Interval List per Device Id    ${onu_device_id}    ${user}    ${group}
+        Activate And Validate Metrics Interval per Onu    ${onu_device_id}    ${list}
+    END
+
+Activate And Validate Metrics Interval per Onu
+    [Documentation]    Activates and validates interval of pm data per onu
+    [Arguments]    ${device_id}    ${group_list}
+    FOR    ${Item}    IN    @{group_list}
+        Continue For Loop If    not '${device_id}' in ${METRIC_DICT}
+        ${group}=    Get From Dictionary     ${Item}    group
+        ${val}=      Get From Dictionary     ${Item}    interval
+        Continue For Loop If    not '${group}' in ${METRIC_DICT['${device_id}']['MetricData']}
+        Continue For Loop If    '${val}'=='-1'
+        # set the unit to sec
+        ${val_with_unit}=    Validate Time Unit    ${val}
+        Set and Validate Group Interval      ${device_id}    ${val_with_unit}    ${group}
+        # update current interval in METRICS_DICT
+        Set To Dictionary    ${METRIC_DICT['${device_id}']['MetricData']['${group}']['Intervals']}    current=${val}
+    END
+
+Set Validation Operation per Onu
+    [Documentation]    Sets group validation data in METRIC_DICT per device id for passed metric element
+    [Arguments]    ${device_id}     ${group}    ${metric_element}    ${validation_dict}
+    Run Keyword If    '${group}' in ${METRIC_DICT['${device_id}']['MetricData']}
+    ...    Set To Dictionary    ${METRIC_DICT['${device_id}']['MetricData']['${group}']['GroupMetrics']['${metric_element}']}
+    ...    ValidationOperation=${validation_dict}
+
+Set Validation Operation All Onu
+    [Documentation]    Sets group validation data in METRIC_DICT all devices and passed metric element
+    [Arguments]    ${group}    ${metric_element}    ${validation_dict}
+    ${onu_list}    Create List
+    FOR    ${INDEX}    IN RANGE    0    ${num_all_onus}
+        ${onu_device_id}=    Get Device ID From SN    ${hosts.src[${INDEX}].onu}
+        ${onu_id}=    Get Index From List    ${onu_list}   ${onu_device_id}
+        Continue For Loop If    -1 != ${onu_id}
+        Continue For Loop If    not '${onu_device_id}' in ${METRIC_DICT}
+        Set Validation Operation per Onu    ${onu_device_id}    ${group}    ${metric_element}    ${validation_dict}
+    END
+
+Set Validation Operation Passed Onu
+    [Documentation]    Sets group validation data in METRIC_DICT for passed devices and passed metric element
+    ...                Passed dictionary has to be format <device_id>:<Validation Dictionary>
+    ...                Keyword 'Get Validation Operation All Onu' delivers such a dictionary.
+    [Arguments]    ${group}    ${metric_element}    ${validation_dict_with_device_id}
+    FOR    ${item}    IN    @{validation_dict_with_device_id.keys()}
+        Continue For Loop If    not '${item}' in ${METRIC_DICT}
+        ${validation_dict}=    Get From Dictionary    ${validation_dict_with_device_id}    ${item}
+        Set Validation Operation per Onu    ${item}    ${group}    ${metric_element}    ${validation_dict}
+    END
+
+Get Validation Operation per Onu
+    [Documentation]    Delivers group validation data in METRIC_DICT per device id for passed metric element
+    [Arguments]    ${device_id}     ${group}    ${metric_element}
+    ${validation_dict}=    Run Keyword If    '${group}' in ${METRIC_DICT['${device_id}']['MetricData']}
+    ...    Get From Dictionary    ${METRIC_DICT['${device_id}']['MetricData']['${group}']['GroupMetrics']['${metric_element}']}
+    ...    ValidationOperation
+    [return]    ${validation_dict}
+
+Get Validation Operation All Onu
+    [Documentation]    Delivers group validation data in METRIC_DICT all devices for passed metric element
+    [Arguments]    ${group}    ${metric_element}
+    ${validation_dict_with_device}    Create Dictionary
+    ${onu_list}    Create List
+    FOR    ${INDEX}    IN RANGE    0    ${num_all_onus}
+        ${onu_device_id}=    Get Device ID From SN    ${hosts.src[${INDEX}].onu}
+        ${onu_id}=    Get Index From List    ${onu_list}   ${onu_device_id}
+        Continue For Loop If    -1 != ${onu_id}
+        Continue For Loop If    not '${onu_device_id}' in ${METRIC_DICT}
+        ${validation_dict}=   Get Validation Operation per Onu    ${onu_device_id}    ${group}    ${metric_element}
+        Set To Dictionary    ${validation_dict_with_device}    ${onu_device_id}=${validation_dict}
+    END
+    [return]    ${validation_dict_with_device}
+
+#################################################################
+# test keywords
+#################################################################
+Collect and Validate PM Data
+    [Documentation]    Collecta PM-data from kafka and validates metrics
+    [Arguments]    ${collect_interval}    ${clear}=True    ${user}=False    ${group}=${EMPTY}
+    ${Kafka_Records}=    Get Metrics    ${collect_interval}    clear=${clear}
+    ${RecordsLength}=    Get Length    ${Kafka_Records}
+    FOR    ${Index}    IN RANGE    0    ${RecordsLength}
+        ${metric}=    Set Variable    ${Kafka_Records[${Index}]}
+        ${message}=    Get From Dictionary  ${metric}  message
+        ${event}=    volthatools.Events Decode Event   ${message}    return_default=true
+        Continue For Loop If    not 'kpi_event2' in ${event}
+        Validate Raised Timestamp   ${event}
+        ${slice}=    Get Slice Data From Event    ${event}
+        Validate Raised Timestamp   ${event}
+        Validate Slice Data   ${slice}    ${Kafka_Records}
+        Set Previous Record    ${Index}    ${slice}
+    END
+    Validate Number of Checks    ${collect_interval}    user=${user}    group=${group}
+
+Get Metrics
+    [Documentation]    Delivers metrics from kafka
+    [Arguments]    ${collect_interval}    ${clear}=True
+    Run Keyword If    ${clear}    kafka.Records Clear
+    Sleep    ${collect_interval}
+    ${Kafka_Records}=    kafka.Records Get    voltha.events
+    [return]    ${Kafka_Records}
+
+Get Title From Metadata
+    [Documentation]    Delivers the title of pm data from metadata
+    [Arguments]    ${metadata}
+    ${title}=    Get From Dictionary    ${metadata}      title
+    [return]    ${title}
+
+Get Device_Id From Metadata
+    [Documentation]    Delivers the device-id of pm data from metadata
+    [Arguments]    ${metadata}
+    ${device_id}=    Get From Dictionary    ${metadata}      device_id
+    [return]    ${device_id}
+
+Get Timestamp From Metadata
+    [Documentation]    Delivers the device-id of pm data from metadata
+    [Arguments]    ${metadata}
+    ${timestamp}=    Get From Dictionary    ${metadata}      ts
+    [return]    ${timestamp}
+
+Get Slice Data From Event
+    [Documentation]    Delivers the slice data of pm data from event
+    [Arguments]    ${event}
+    ${kpi_event2}=    Get From Dictionary    ${event}         kpi_event2
+    ${slice_data}=    Get From Dictionary    ${kpi_event2}    slice_data
+    [return]    ${slice_data}
+
+Validate Raised Timestamp
+    [Documentation]    Validates raisedTs with kpi_event2 ts
+    ...                It is assumed kpi_event2 ts and raisedTs from event header should be the same
+    [Arguments]    ${event}
+    ${raisedTs}=        Get From Dictionary  ${event['header']['raised_ts']}      seconds
+    ${reportedTs}=      Get From Dictionary  ${event['header']['reported_ts']}    seconds
+    ${kpiEvent2Ts}=     Get From Dictionary  ${event['kpi_event2']}               ts
+    # ${raisedTs} and ${reportedTs} looks like '2021-04-28 06:56:54.000000', convert it to seconds of epoch
+    ${raisedTsSec}=      Convert Date    ${raisedTs}      result_format=epoch    exclude_millis=yes
+    ${reportedTsSec}=    Convert Date    ${reportedTs}    result_format=epoch    exclude_millis=yes
+    Run Keyword And Continue On Failure    Should Be Equal    ${raisedTsSec}    ${kpiEvent2Ts}
+    ${result}=    utility.validate    ${raisedTsSec}    <=    ${reportedTsSec}
+    ${msg}=    Catenate    raisedTs (${raisedTs}) must be earlier or equal than reportedTs (${reportedTs})!
+    Run Keyword Unless    ${result}    Run Keyword And Continue On Failure    FAIL    ${msg}
+
+Validate Slice Data
+    [Documentation]    Validates passed slice data
+    [Arguments]    ${slice}    ${metric_records}
+    ${result}=       Set Variable    True
+    ${checked}=      Set Variable    False
+    ${device_id}=    Set Variable    ${EMPTY}
+    ${title}=        Set Variable    ${EMPTY}
+    ${SliceLength}=    Get Length    ${slice}
+    Validate Slice Metrics Integrity    ${slice}
+    FOR    ${Index}    IN RANGE    0    ${SliceLength}
+        ${metadata}=    Get From Dictionary    ${slice[${Index}]}    metadata
+        ${metrics}=     Get From Dictionary    ${slice[${Index}]}    metrics
+        ${title}=    Get Title From Metadata    ${metadata}
+        ${device_id}=    Get Device_Id From Metadata    ${metadata}
+        Continue For Loop If    not '${device_id}' in ${METRIC_DICT}
+        ${timestamp}=    Get Timestamp From Metadata    ${metadata}
+        ${prevSliceIndex}=    Run Keyword If     'PreviousRecord' in ${METRIC_DICT['${device_id}']['MetricData']['${title}']}
+        ...    Get Previous Slice Index    ${device_id}    ${title}    ${metric_records}    ${metrics}
+        ...    ELSE    Set Variable    0
+        Continue For Loop If    ${prevSliceIndex}==-1
+        Run Keyword If     'PreviousRecord' in ${METRIC_DICT['${device_id}']['MetricData']['${title}']}
+        ...    Validate Timestamp    ${device_id}    ${title}    ${timestamp}    ${metric_records}    ${prevSliceIndex}
+        ${hasprev}=    Set Variable If     'PreviousRecord' in ${METRIC_DICT['${device_id}']['MetricData']['${title}']}
+        ...            True    False
+        Validate Metrics Data   ${device_id}    ${title}    ${metrics}    ${hasprev}    ${metric_records}    ${prevSliceIndex}
+        Validate Completeness of Metrics Data   ${device_id}    ${title}    ${metrics}
+        ${checked}=    Set Variable    True
+    END
+    # increase number of checks, only once per slice
+    Run Keyword If    ${checked}    Increase Number of Checks    ${device_id}    ${title}
+
+Validate Slice Metrics Integrity
+    [Documentation]    Valitdates the inegrity of the passed slice.
+    ...                The pair 'entity_id' and 'class_id' has tor appear only once per slice!
+    ...                In case of metric group UNI_Status parameter 'uni_port_no' appends to check!
+    [Arguments]    ${slice}
+    ${prev_values}    Create List
+    ${SliceLength}=    Get Length    ${slice}
+    FOR    ${Index}    IN RANGE    0    ${SliceLength}
+        ${metadata}=    Get From Dictionary    ${slice[${Index}]}    metadata
+        ${metrics}=     Get From Dictionary    ${slice[${Index}]}    metrics
+        ${title}=    Get Title From Metadata    ${metadata}
+        # get entity-id and class_id if available
+        # class_id identifier differs in case of metric group UNI_Status, 'me_class_id' instead simple 'class_id'
+        ${class_id_name}=   Run Keyword If    '${title}'=='UNI_Status'   Set Variable    me_class_id
+        ...    ELSE    Set Variable    class_id
+        Continue For Loop If    not 'entity_id' in ${metrics}
+        Continue For Loop If    not '${class_id_name}' in ${metrics}
+        ${entity_id}=    Get From Dictionary    ${metrics}    entity_id
+        ${class_id}=     Get From Dictionary    ${metrics}    ${class_id_name}
+        # additional handling for metric group UNI_Status, uni_port_no has to be matched too
+        ${uni_port_no}=    Run Keyword If    '${title}'=='UNI_Status'    Get From Dictionary    ${metrics}    uni_port_no
+        ...    ELSE    Set Variable    ${Index}
+        ${current_values}=    Create Dictionary    entity_id=${entity_id}    class_id=${class_id}    uni_port_no=${uni_port_no}
+        Run Keyword And Continue On Failure    Should Not Contain    ${prev_values}    ${current_values}
+        Append To List    ${prev_values}    ${current_values}
+    END
+
+Get Previous Slice Index
+    [Documentation]    Delivers the slice index of previous metrics.
+    ...                Previous slice index will be identified by matching entity_ids.
+    ...                In case of UNI_Status the me_class_id has to be matched too!
+    [Arguments]    ${device_id}    ${title}    ${metric_records}    ${metrics}
+    ${prevSliceIndex}=    Set Variable    0
+    # get entity-id and class_id if available
+    # class_id identifier differs in case of metric group UNI_Status, 'me_class_id' instead simple 'class_id'
+    ${class_id_name}=   Run Keyword If    '${title}'=='UNI_Status'   Set Variable    me_class_id
+    ...    ELSE    Set Variable    class_id
+    Return From Keyword If    not 'entity_id' in ${metrics}    ${prevSliceIndex}
+    Return From Keyword If    not '${class_id_name}' in ${metrics}    ${prevSliceIndex}
+    ${entity_id}=    Get From Dictionary    ${metrics}    entity_id
+    ${class_id}=     Get From Dictionary    ${metrics}    ${class_id_name}
+    # get previous entity-id
+    ${prev_index}=    Set Variable    ${METRIC_DICT['${device_id}']['MetricData']['${title}']['PreviousRecord']}
+    ${pre_record}=    Set Variable    ${metric_records[${prev_index}]}
+    ${message}=       Get From Dictionary  ${pre_record}  message
+    ${event}=         volthatools.Events Decode Event   ${message}    return_default=true
+    ${prev_slice}=    Get Slice Data From Event    ${event}
+    ${prevSliceLength}=   Get Length    ${prev_slice}
+    ${matched}=       Set Variable    False
+    FOR    ${Index}    IN RANGE    0    ${prevSliceLength}
+        ${prevmetrics}=    Get From Dictionary    ${prev_slice[${Index}]}    metrics
+        ${prev_entity_id}=    Get From Dictionary    ${prevmetrics}    entity_id
+        ${prev_class_id}=    Get From Dictionary    ${prevmetrics}    ${class_id_name}
+        ${matched}=    Set Variable If   (${entity_id}==${prev_entity_id})and(${${class_id}}==${prev_class_id})   True   False
+        ${prevSliceIndex}=    Set Variable If    ${matched}    ${Index}    -1
+        Exit For Loop If    ${matched}
+    END
+    Run Keyword And Continue On Failure    Run Keyword Unless    ${matched}    FAIL
+    ...    Could not find previous metrics for ${title} entity_id ${entity_id} of device ${device_id}!
+    [return]    ${prevSliceIndex}
+
+Validate Timestamp
+    [Documentation]    Validates passed timestamp with timestamp of previous metrics
+    [Arguments]    ${device_id}    ${title}    ${timestamp}    ${metric_records}    ${prevSliceIndex}
+    # get previous timestamp
+    ${prev_timestamp}=   Get Previous Timestamp    ${METRIC_DICT['${device_id}']['MetricData']['${title}']['PreviousRecord']}
+    ...    ${metric_records}    ${prevSliceIndex}
+    ${interval}=    Get From Dictionary  ${METRIC_DICT['${device_id}']['MetricData']['${title}']['Intervals']}
+    ...    current
+    ${interval}=    Convert To Integer    ${interval}
+    ${check_value}=     Evaluate    abs(${prev_timestamp}+${interval}-${timestamp})
+    Run Keyword And Continue On Failure    Run Keyword Unless    ${0} <= ${check_value} <= ${4}    FAIL
+    ...    Wrong interval for ${title} of device ${device_id}!
+
+Get Validation Operation
+    [Documentation]    Delivers the stored validation operation of passed metrics
+    [Arguments]    ${dev_id}    ${title}    ${item}    ${has_previous}
+    ${w_wo_prev}=    Set Variable If    ${has_previous}    successor    first
+    ${validation_operator}=    Get From Dictionary
+    ...   ${METRIC_DICT['${dev_id}']['MetricData']['${title}']['GroupMetrics']['${item}']['ValidationOperation']['${w_wo_prev}']}
+    ...   operator
+    ${validation_operand}=    Get From Dictionary
+    ...   ${METRIC_DICT['${dev_id}']['MetricData']['${title}']['GroupMetrics']['${item}']['ValidationOperation']['${w_wo_prev}']}
+    ...   operand
+    [return]    ${validation_operator}    ${validation_operand}
+
+Get Previous Value
+    [Documentation]    Delivers the previous value
+    [Arguments]    ${device_id}    ${title}    ${item}    ${metric_records}    ${prevSliceIndex}
+    ${prev_index}=    Set Variable    ${METRIC_DICT['${device_id}']['MetricData']['${title}']['PreviousRecord']}
+    ${pre_record}=    Set Variable    ${metric_records[${prev_index}]}
+    ${message}=       Get From Dictionary  ${pre_record}  message
+    ${event}=         volthatools.Events Decode Event   ${message}    return_default=true
+    ${slice}=         Get Slice Data From Event    ${event}
+    ${metrics}=       Get From Dictionary    ${slice[${prevSliceIndex}]}    metrics
+    ${prev_value}=    Get From Dictionary    ${metrics}    ${item}
+    [return]    ${prev_value}
+
+Validate Metrics Data
+    [Documentation]    Validates passed metrics
+    [Arguments]    ${device_id}    ${title}    ${metrics}    ${has_previous}    ${metric_records}    ${prevSliceIndex}
+    FOR    ${item}    IN    @{metrics.keys()}
+        ${operation}    ${validation_value}=    Get Validation Operation   ${device_id}    ${title}    ${item}    ${has_previous}
+        # get previous value in case of ${has_previous}==True and ${validation_value}==previous
+        ${validation_value}=    Run Keyword If    ${has_previous} and '${validation_value}'=='previous'   Get Previous Value
+        ...    ${device_id}    ${title}    ${item}    ${metric_records}    ${prevSliceIndex}
+        ...    ELSE    Set Variable    ${validation_value}
+        ${current_value}=    Get From Dictionary    ${metrics}    ${item}
+        ${result}=    utility.validate    ${current_value}    ${operation}    ${validation_value}
+        ${msg}=    Catenate    Received value (${current_value}) from device (${device_id}) of group (${title}) for '${item}'
+        ...    does not match!
+        ...    Expected: <value> ${operation} ${validation_value}
+        Run Keyword Unless    ${result}    Run Keyword And Continue On Failure    FAIL    ${msg}
+    END
+
+Validate Precondition for Availability
+    [Documentation]    Validates passed metrics for stored precondition
+    [Arguments]    ${device_id}    ${title}    ${metrics}     ${item}
+    ${precond}=    Get From Dictionary    ${METRIC_DICT['${device_id}']['MetricData']['${title}']['GroupMetrics']['${item}']}
+    ...    Precondition
+    ${operation}=           Get From Dictionary    ${precond}    operator
+    ${validation_value}=    Get From Dictionary    ${precond}    operand
+    ${element}=             Get From Dictionary    ${precond}    element
+    ${current_value}=       Get From Dictionary    ${metrics}    ${element}
+    ${result}=    utility.validate    ${current_value}    ${operation}    ${validation_value}
+    [return]    ${result}
+
+Validate Completeness of Metrics Data
+    [Documentation]    Validates passed metrics of completness
+    [Arguments]    ${device_id}    ${title}    ${metrics}
+    # get validation data
+    ${validation_data}=    Set Variable    ${METRIC_DICT['${device_id}']['MetricData']['${title}']['GroupMetrics']}
+    FOR    ${item}    IN    @{validation_data.keys()}
+        ${precondfullfilled}=    Run Keyword If
+        ...    'Precondition' in ${METRIC_DICT['${device_id}']['MetricData']['${title}']['GroupMetrics']['${item}']}
+        ...    Validate Precondition for Availability    ${device_id}    ${title}    ${metrics}     ${item}
+        ...    ELSE    Set Variable    True
+        Run Keyword If    (not '${item}' in ${metrics}) and ${precondfullfilled}    Run Keyword And Continue On Failure    FAIL
+        ...    Missing metric (${item}) from device (${device_id}) of group (${title})
+    END
+
+Get Previous Timestamp
+    [Documentation]    Deliveres the timestamp of the passed metrics record
+    [Arguments]    ${prev_index}    ${metric_records}    ${prevSliceIndex}
+    ${metric}=    Set Variable    ${metric_records[${prev_index}]}
+    ${message}=    Get From Dictionary  ${metric}  message
+    ${event}=    volthatools.Events Decode Event   ${message}    return_default=true
+    ${slice}=    Get Slice Data From Event    ${event}
+    ${metadata}=    Get From Dictionary    ${slice[${prevSliceIndex}]}    metadata
+    ${timestamp}=    Get Timestamp From Metadata    ${metadata}
+    [return]    ${timestamp}
+
+Set Previous Record
+    [Documentation]    Sets the previous record in METRIC_DICT for next validation
+    [Arguments]    ${Index}    ${slice}
+    # use first slice for further handling
+    ${metadata}=    Get From Dictionary    ${slice[${0}]}    metadata
+    ${title}=    Get Title From Metadata    ${metadata}
+    ${device_id}=    Get Device_Id From Metadata    ${metadata}
+    Return From Keyword If    not '${device_id}' in ${METRIC_DICT}
+    ${metric}=    Get From Dictionary     ${METRIC_DICT['${device_id}']['MetricData']}    ${title}
+    Set To Dictionary    ${metric}    PreviousRecord=${Index}
+    Set To Dictionary    ${METRIC_DICT['${device_id}']['MetricData']}    ${title}=${metric}
+
+Increase Number of Checks
+    [Documentation]    Increases the NumberOfChecks value in METRIC_DICT for passed device id and title
+    [Arguments]    ${device_id}    ${title}
+    ${checks}=    Get From Dictionary    ${METRIC_DICT['${device_id}']['MetricData']['${title}']}    NumberOfChecks
+    ${checks}=    evaluate    ${checks} + 1
+    Set To Dictionary    ${METRIC_DICT['${device_id}']['MetricData']['${title}']}    NumberOfChecks=${checks}
+
+Validate Number of Checks
+    [Documentation]    Validates the NumberOfChecks value in METRIC_DICT, must be at least >=2
+    [Arguments]    ${collect_interval}    ${user}=False    ${group}=${EMPTY}
+    ${onu_list}    Create List
+    FOR    ${INDEX}    IN RANGE    0    ${num_all_onus}
+        ${onu_device_id}=    Get Device ID From SN    ${hosts.src[${INDEX}].onu}
+        ${onu_id}=    Get Index From List    ${onu_list}   ${onu_device_id}
+        Continue For Loop If    -1 != ${onu_id}
+        ${list}=    Prepare Group Interval List per Device Id    ${onu_device_id}    ${user}    ${group}
+        Validate Number of Checks per Onu    ${onu_device_id}    ${list}    ${collect_interval}
+    END
+
+Validate Number of Checks per Onu
+    [Documentation]    Validates the NumberOfChecks value per ONU, must be at least >=2
+    ...                Collecting of metrics will be calculated that at least each group has to be checked twice!
+    ...                Correct value per group and its interval will be calculated!
+    [Arguments]    ${device_id}    ${list}    ${collect_interval}
+    FOR    ${Item}    IN    @{list}
+        ${group}=    Get From Dictionary     ${Item}    group
+        ${val}=      Get From Dictionary     ${Item}    interval
+        # use interval value to skip groups, which should not be checked!
+        Continue For Loop If    '${val}'=='-1'
+        ${checks}=    Get From Dictionary    ${METRIC_DICT['${device_id}']['MetricData']['${group}']}    NumberOfChecks
+        # remove time unit if available
+        ${collect_interval}=    Validate Time Unit    ${collect_interval}    False
+        ${expected_checks}=    evaluate    ${collect_interval}/${val}
+        # remove float format (Validate Time Unit will this done:-))
+        ${expected_checks}=    Validate Time Unit    ${expected_checks}    False
+        Run Keyword And Continue On Failure    Run Keyword Unless    ${expected_checks} <= ${checks}    FAIL
+        ...    Wrong number of pm-data (${checks}) for ${group} of device ${device_id}!
+    END
+
+#################################################################
+# Post test keywords
+#################################################################
+
+Remove Previous Record
+    [Documentation]    Removes the previous record in METRIC_DICT
+    [Arguments]    ${device_id}    ${title}
+    Remove From Dictionary    ${METRIC_DICT['${device_id}']['MetricData']['${title}']}    PreviousRecord
+
+Reset Number Of Checks
+    [Documentation]    Removes the previous record in METRIC_DICT
+    [Arguments]    ${device_id}    ${title}
+    Set To Dictionary    ${METRIC_DICT['${device_id}']['MetricData']['${title}']}    NumberOfChecks=0
+
+Clean Metric Dictionary per Onu
+    [Documentation]    Cleans METRIC_DICT per onu device id
+    [Arguments]    ${device_id}
+    FOR    ${Item}    IN     @{METRIC_DICT['${device_id}']['GroupList']}
+        Remove Previous Record    ${device_id}    ${Item}
+        Reset Number Of Checks    ${device_id}    ${Item}
+    END
+
+Clean Metric Dictionary
+    [Documentation]    Cleans METRIC_DICT
+    ${onu_list}    Create List
+    FOR    ${INDEX}    IN RANGE    0    ${num_all_onus}
+        ${onu_device_id}=    Get Device ID From SN    ${hosts.src[${INDEX}].onu}
+        ${onu_id}=    Get Index From List    ${onu_list}   ${onu_device_id}
+        Continue For Loop If    -1 != ${onu_id}
+        Clean Metric Dictionary per Onu    ${onu_device_id}
+    END
+    log    ${METRIC_DICT}
+
+
+#################################################################
+# Helper keywords
+#################################################################
+
+Validate Time Unit
+    [Documentation]    Converts the passed value in seconds and return it w/o unit
+    ...                Conversion to string is needed to remove float format!
+    [Arguments]    ${val}    ${unit}=True
+    ${seconds}=    Convert Time    ${val}
+    ${seconds}=    Convert To String    ${seconds}
+    ${seconds}=    Get Substring    ${seconds}    0    -2
+    ${seconds}=    Set Variable If    ${unit}    ${seconds}s    ${seconds}
+    [return]    ${seconds}
diff --git a/libraries/utility.py b/libraries/utility.py
index 6faec6e..9da6a98 100755
--- a/libraries/utility.py
+++ b/libraries/utility.py
@@ -17,6 +17,7 @@
 from __future__ import print_function
 import inspect
 import os
+import operator
 
 # global definition of keys (find in given 'inventory_data')
 _NAME = 'name'
@@ -103,3 +104,35 @@
 def decode(data):
     decoded_data = data
     print(unique(), str(decoded_data))
+
+
+# Compares two values using a given operator. The values are converted to float first so that
+# numbers as strings are also accepted. Returns True or False.
+# operator: ==, !=, <, <=, >, >=
+# Example:
+# | ${result} | Compare | 100 | >  | 5  | # True |
+def compare(value1, op, value2):
+    ops = {"==": operator.eq,
+           "!=": operator.ne,
+           "<":  operator.lt,
+           "<=": operator.le,
+           ">":  operator.gt,
+           ">=": operator.ge}
+    return ops[op](float(value1), float(value2))
+
+
+# Validates two values using a given operator.
+# The values are converted to float first so that numbers as strings are also accepted.
+# Second value has to be a list in case of operator is 'in' or 'range'
+# Returns True or False.
+# operator: in, range, ==, !=, <, <=, >, >=
+# Example:
+# | ${result} | validate | 100 | >  | 5  | # True |
+# | ${result} | validate | 11  | in | ['11','264','329']  | # True |
+# | ${result} | validate | 1   | range | ['0','1']  | # True |
+def validate(value1, op, value2):
+    if op == "in":
+        return (float(value1) in [float(i) for i in value2])
+    if op == "range":
+        return ((compare(value1, ">=", value2[0])) and (compare(value1, "<=", value2[1])))
+    return compare(value1, op, value2)
diff --git a/libraries/voltctl.robot b/libraries/voltctl.robot
index 79c61e3..4df47bf 100755
--- a/libraries/voltctl.robot
+++ b/libraries/voltctl.robot
@@ -833,3 +833,103 @@
     Should Be Equal    '${downloadstate}'    '${download_state}'    ONU Device ${id} Image downloadState does not match
     Should Be Equal    '${imagestate}'    '${image_state}'    ONU Device ${id} Image imageState does not match
     Should Be Equal    '${reason}'    '${expected_reason}'    ONU Device ${id} Image reason does not match
+
+
+# pm-data relevant keywords
+Read Default Interval From Pmconfig
+    [Documentation]    Reads default interval from pm config
+    [Arguments]    ${device_id}
+    ${rc}    ${result}=    Run and Return Rc and Output    voltctl device pmconfig get ${device_id}
+    Should Be Equal As Integers    ${rc}    0
+    log    ${result}
+    @{words}=    Split String    ${result}
+    ${interval}=    Get From List    ${words}    3
+    log    ${interval}
+    [return]    ${interval}
+
+Read Group Interval From Pmconfig
+    [Documentation]    Reads default interval from pm config
+    [Arguments]    ${device_id}    ${group}
+    ${rc}    ${result}=    Run and Return Rc and Output     voltctl device pmconfig group list ${device_id} | grep ${group}
+    Should Be Equal As Integers    ${rc}    0
+    log    ${result}
+    @{words}=    Split String    ${result}
+    ${interval}=    Get From List    ${words}    -1
+    log    ${interval}
+    [return]    ${interval}
+
+Set and Validate Default Interval
+    [Documentation]    Sets and validates default interval of pm data
+    [Arguments]    ${device_id}    ${interval}
+    ${rc}    ${result}=    Run and Return Rc and Output    voltctl device pmconfig frequency set ${device_id} ${interval}
+    Should Be Equal As Integers    ${rc}    0
+    log    ${result}
+    # workaround until unit will be printed out in voltctl - remove unit
+    ${interval}=    Get Substring    ${interval}    0    -1
+    Should Contain    ${result}    ${interval}
+
+Set and Validate Group Interval
+    [Documentation]    Sets and validates group interval of pm data
+    [Arguments]    ${device_id}    ${interval}    ${group}
+    ${rc}    ${result}=    Run and Return Rc and Output    voltctl device pmconfig group set ${device_id} ${group} ${interval}
+    Should Be Equal As Integers    ${rc}    0
+    ${rc}    ${result}=    Run and Return Rc and Output     voltctl device pmconfig group list ${device_id} | grep ${group}
+    Should Be Equal As Integers    ${rc}    0
+    log    ${result}
+    # workaround until unit will be printed out in voltctl - remove unit
+    ${interval}=    Get Substring    ${interval}    0    -1
+    Should Contain    ${result}    ${interval}
+
+Read Group List
+    [Documentation]    Reads metric group list of given device
+    [Arguments]    ${device_id}
+    ${rc}    ${result}=    Run and Return Rc and Output    voltctl device pmconfig group list ${device_id} | grep -v GROUPNAME
+    Should Be Equal As Integers    ${rc}    0
+    ${group_list}    Create List
+    ${interval_dict}     Create Dictionary
+    @{output}=    Split String    ${result}    \n
+    FOR    ${Line}    IN     @{output}
+        @{words}=    Split String    ${Line}
+        ${group}=    Set Variable    ${words[0]}
+        ${interval}=    Set Variable    ${words[2]}
+        Append To List    ${group_list}    ${group}
+        Set To Dictionary    ${interval_dict}    ${group}=${interval}
+    END
+    [return]    ${group_list}    ${interval_dict}
+
+Read Group Metric List
+    [Documentation]    Reads group metric list of given device and group
+    [Arguments]    ${device_id}    ${group}
+    ${cmd}=    Catenate    voltctl device pmconfig groupmetric list ${device_id} ${group} | grep -v SAMPLEFREQ
+    ${rc}    ${result}=    Run and Return Rc and Output    ${cmd}
+    Should Be Equal As Integers    ${rc}    0
+    ${groupmetric_list}    Create List
+    @{output}=    Split String    ${result}    \n
+    FOR    ${Line}    IN     @{output}
+        @{words}=      Split String    ${Line}
+        ${name}=      Set Variable    ${words[0]}
+        ${type}=       Set Variable    ${words[1]}
+        ${enabled}=    Set Variable    ${words[2]}
+        ${subdict}=       Create Dictionary    type=${type}    enabled=${enabled}
+        ${dict}=       Create Dictionary    ${name}=${subdict}
+        Append To List    ${groupmetric_list}    ${dict}
+    END
+    [return]    ${groupmetric_list}
+
+Read Group Metric Dict
+    [Documentation]    Reads group metric list of given device and group
+    [Arguments]    ${device_id}    ${group}
+    ${cmd}=    Catenate    voltctl device pmconfig groupmetric list ${device_id} ${group} | grep -v SAMPLEFREQ
+    ${rc}    ${result}=    Run and Return Rc and Output    ${cmd}
+    Should Be Equal As Integers    ${rc}    0
+    ${groupmetric_dict}    Create Dictionary
+    @{output}=    Split String    ${result}    \n
+    FOR    ${Line}    IN     @{output}
+        @{words}=      Split String    ${Line}
+        ${name}=      Set Variable    ${words[0]}
+        ${type}=       Set Variable    ${words[1]}
+        ${enabled}=    Set Variable    ${words[2]}
+        ${subdict}=       Create Dictionary    type=${type}    enabled=${enabled}
+        Set To Dictionary    ${groupmetric_dict}    ${name}=${subdict}
+    END
+    [return]    ${groupmetric_dict}
diff --git a/requirements.txt b/requirements.txt
old mode 100644
new mode 100755
index 39b5499..b53f166
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,6 +13,9 @@
 virtualenv
 matplotlib==3.0.3
 requests==2.24.0
-grpc-robot>=2.0.0
-kafka-robot>=2.0.0
-device-management-interface>=0.9.4
\ No newline at end of file
+grpc-robot>=2.7.0
+kafka-robot>=2.3.2
+voltha-protos>=4.0.16
+protobuf>=3.15.7
+protobuf3-to-dict>=0.1.5
+device-management-interface>=0.13.0
\ No newline at end of file
diff --git a/tests/data/pm-data.yaml b/tests/data/pm-data.yaml
new file mode 100755
index 0000000..2203531
--- /dev/null
+++ b/tests/data/pm-data.yaml
@@ -0,0 +1,72 @@
+---
+
+# Copyright 2021-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Automated deployment configuration for kind-voltha running BBSim
+
+# Pm user validation and precondition data for validation of received
+# pm-data elements from open-onu-go-adapter.
+# This file contains special validation data, which differs from default
+# validation data.
+# Following operators are valid: in, range, ==, !=, <, <=, >, >=
+# In case of operator is 'in' or 'range' second value has to be a list (array)
+
+pm_user_validation_data:
+  UNI_Status:
+    me_class_id:
+      firstoperator: in
+      firstvalue:
+        - 11
+        - 264
+        - 329
+      successoroperator: ==
+      successorvalue: previous
+    oper_status:
+      firstoperator: range
+      firstvalue:
+        - 0
+        - 1
+      successoroperator: range
+      successorvalue:
+        - 0
+        - 1
+    uni_admin_state:
+      firstoperator: range
+      firstvalue:
+        - 0
+        - 1
+      successoroperator: range
+      successorvalue:
+        - 0
+        - 1
+    sensed_type:
+      firstoperator: range
+      firstvalue:
+        - 0
+        - 255
+      successoroperator: ==
+      successorvalue: previous
+pm_user_precondition_data:
+  UNI_Status:
+    sensed_type:
+      operator: ==
+      value: 11
+      precondelement: me_class_id
+    oper_status:
+      operator: in
+      value:
+        - 11
+        - 329
+      precondelement: me_class_id
diff --git a/tests/pm-data/Voltha_ONUPMTests.robot b/tests/pm-data/Voltha_ONUPMTests.robot
new file mode 100755
index 0000000..1623557
--- /dev/null
+++ b/tests/pm-data/Voltha_ONUPMTests.robot
@@ -0,0 +1,215 @@
+# Copyright 2021 - present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+*** Settings ***
+Documentation     Test of open ONU go adapter PM data
+...               in case of kafka pod runs in k8s cluster - kafka has to deploy with following EXTRA_HELM_FLAGS
+...               --set externalAccess.enabled=true,
+...               --set externalAccess.service.type=NodePort,
+...               --set externalAccess.service.nodePorts[0]=${KAFKA_PORT},
+...               --set externalAccess.service.domain=${KAFKA_IP}
+...               with e.g. service.domain=10.0.02.15 or 127.0.0.1 and service.nodePorts[0]=30201!
+Suite Setup       Setup Suite
+Suite Teardown    Teardown Suite
+Test Setup        Setup
+Test Teardown     Teardown
+Library           Collections
+Library           String
+Library           OperatingSystem
+Library           XML
+Library           RequestsLibrary
+Library           ../../libraries/DependencyLibrary.py
+Resource          ../../libraries/onos.robot
+Resource          ../../libraries/voltctl.robot
+Resource          ../../libraries/voltha.robot
+Resource          ../../libraries/utils.robot
+Resource          ../../libraries/k8s.robot
+Resource          ../../libraries/onu_utilities.robot
+Resource          ../../libraries/bbsim.robot
+Resource          ../../libraries/pm_utilities.robot
+Resource          ../../variables/variables.robot
+
+Library           kafka_robot.KafkaClient    log_level=DEBUG    WITH NAME    kafka
+Library           grpc_robot.VolthaTools     WITH NAME    volthatools
+
+
+*** Variables ***
+${namespace}      voltha
+${timeout}        60s
+${of_id}          0
+${logical_id}     0
+${has_dataplane}    True
+${external_libs}    True
+${teardown_device}    True
+${scripts}        ../../scripts
+# Per-test logging on failure is turned off by default; set this variable to enable
+${container_log_dir}    ${None}
+
+# logging flag to enable Collect Logs, can be passed via the command line too
+# example: -v logging:True
+${logging}    False
+# determines the environment workflow: DT, TT or ATT (default)
+# example: -v workflow:DT
+${workflow}    ATT
+# when voltha is running in k8s port forwarding is needed
+# example: -v PORT_FORWARDING:False
+${PORT_FORWARDING}    True
+# kafka ip e.g. ip of master host where k8s is running
+# example: -v KAFKA_IP:10.0.2.15
+${KAFKA_IP}    127.0.0.1
+# kafka port: port of kafka nodeport
+# example: -v KAFKA_PORT:30201
+${KAFKA_PORT}    30201
+# kafka service port: service port of kafka nodeport
+# example: -v KAFKA_SVC_PORT:9094
+${KAFKA_SVC_PORT}    9094
+# onu pm data default interval
+# example: -v ONU_DEFAULT_INTERVAL:50s
+${ONU_DEFAULT_INTERVAL}    300s
+# onu pm data group PON_Optical interval
+# example: -v ONU_PON_OPTICAL_INTERVAL:50s
+${ONU_PON_OPTICAL_INTERVAL}    35s
+# onu pm data group UNI_Status interval
+# example: -v ONU_UNI_STATUS_INTERVAL:50s
+${ONU_UNI_STATUS_INTERVAL}    20s
+
+*** Test Cases ***
+Check Default Metrics All ONUs
+    [Documentation]    Validates the ONU Go adapter pm date resp. Metrics with dafault values
+    [Tags]    functional    CheckDefaultMetricsAllOnus
+    [Setup]   Start Logging    CheckDefaultMetricsAllOnus
+    ${collect_interval}=    Determine Collection Interval
+    Collect and Validate PM Data    ${collect_interval}
+    [Teardown]    Run Keywords    Clean Metric Dictionary
+    ...    AND    Run Keyword If    ${logging}    Collect Logs
+    ...    AND    Stop Logging    CheckDefaultMetricsAllOnus
+
+Check User Onu Metrics
+    [Documentation]    Validates the ONU Go adapter pm date resp. Metrics with user values
+    ...                Currently only the intvals of metric groups UNI_Status and PON_Optical will be set to user values.
+    [Tags]    functional    CheckUserOnuMetrics
+    [Setup]   Start Logging    CheckUserOnuMetrics
+    # set user values for intervals
+    Set Group Interval All Onu    UNI_Status    ${ONU_UNI_STATUS_INTERVAL}
+    Set Group Interval All Onu    PON_Optical   ${ONU_PON_OPTICAL_INTERVAL}
+    ${collect_interval}=    Determine Collection Interval    user=True
+    # activate user interval values
+    Activate And Validate Interval All Onu    user=True
+    Collect and Validate PM Data    ${collect_interval}    user=True
+    # (re-)activate default interval values
+    Set Group Interval All Onu    UNI_Status    -1
+    Set Group Interval All Onu    PON_Optical   -1
+    Activate And Validate Interval All Onu
+    [Teardown]    Run Keywords    Clean Metric Dictionary
+    ...    AND    Run Keyword If    ${logging}    Collect Logs
+    ...    AND    Stop Logging    CheckUserOnuMetrics
+
+Check User Onu Metrics Disabled Device
+    [Documentation]    Validates the ONU Go adapter pm date resp. Metrics with user values for disabled device
+    ...                Currently only the intvals of metric groups UNI_Status will be set to user values and validated.
+    ...                First enable status will be validated, then all devices will be disabled and status will be validated.
+    [Tags]    functional    CheckUserOnuMetricsDisabledDevice
+    [Setup]   Start Logging    CheckUserOnuMetricsDisabledDevice
+    # set user values for intervals
+    Set Group Interval All Onu    UNI_Status    ${ONU_UNI_STATUS_INTERVAL}
+    ${collect_interval}=    Determine Collection Interval    user=True
+    # activate user interval values
+    Activate And Validate Interval All Onu    user=True
+    # read and store currents validation data
+    ${group}=    Set Variable    UNI_Status
+    ${oper_state}=    Set Variable    oper_status
+    ${admin_state}=   Set Variable    uni_admin_state
+    ${prev_validation_data_oper_state}=     Get Validation Operation All Onu   ${group}    ${oper_state}
+    ${prev_validation_data_admin_state}=    Get Validation Operation All Onu   ${group}    ${admin_state}
+    # change the validation data for oper_status and uni_admin_state of metric group UNI_Status
+    ${enable}=        Set Variable    0
+    ${disable}=       Set Variable    1
+    ${enabled_check}=    Create Dictionary    operator=${eq}    operand=${enable}
+    ${disabled_check}=   Create Dictionary    operator=${eq}    operand=${disable}
+    ${ValidationEnabled}=    Create Dictionary    first=${enabled_check}    successor=${enabled_check}
+    ${ValidationDisabled}=   Create Dictionary    first=${disabled_check}   successor=${disabled_check}
+    # validate enabled status
+    Set Validation Operation All Onu    ${group}    ${oper_state}    ${ValidationEnabled}
+    Set Validation Operation All Onu    ${group}    ${admin_state}   ${ValidationEnabled}
+    Collect and Validate PM Data    ${collect_interval}    user=True
+    Clean Metric Dictionary
+    # validate disabled status
+    Set Validation Operation All Onu    ${group}    ${oper_state}    ${ValidationDisabled}
+    Set Validation Operation All Onu    ${group}    ${admin_state}   ${ValidationDisabled}
+    # disable (all) onu devices
+    Disable Onu Device
+    ${alternativeonustates}=  Create List     omci-flows-deleted    tech-profile-config-delete-success
+    Current State Test All Onus    omci-admin-lock    alternativeonustate=${alternativeonustates}
+    Log Ports
+    #check no port is enabled in ONOS
+    Wait for Ports in ONOS for all OLTs    ${onos_ssh_connection}    0    BBSM
+    Collect and Validate PM Data    ${collect_interval}    user=True
+    Clean Metric Dictionary
+    # enable (all) onu devices
+    Enable Onu Device
+    ${alternativeonustates}=  Create List     onu-reenabled
+    Current State Test All Onus    omci-flows-pushed    alternativeonustate=${alternativeonustates}
+    Log Ports    onlyenabled=True
+    #check that all the UNI ports show up in ONOS again
+    Wait for Ports in ONOS for all OLTs    ${onos_ssh_connection}    ${num_all_onus}    BBSM    determine_number=True
+    # validate enabled status (again)
+    Set Validation Operation All Onu    ${group}    ${oper_state}    ${ValidationEnabled}
+    Set Validation Operation All Onu    ${group}    ${admin_state}   ${ValidationEnabled}
+    Collect and Validate PM Data    ${collect_interval}    user=True
+    # (re-)set previous validation data
+    Set Validation Operation Passed Onu   ${group}    ${oper_state}     ${prev_validation_data_oper_state}
+    Set Validation Operation Passed Onu   ${group}    ${admin_state}    ${prev_validation_data_admin_state}
+    # (re-)activate default interval values
+    Set Group Interval All Onu    UNI_Status    -1
+    Activate And Validate Interval All Onu
+    [Teardown]    Run Keywords    Clean Metric Dictionary
+    ...    AND    Run Keyword If    ${logging}    Collect Logs
+    ...    AND    Stop Logging    CheckUserOnuMetricsDisabledDevice
+
+
+*** Keywords ***
+Setup Suite
+    [Documentation]    Set up the test suite
+    Common Test Suite Setup
+    ${onos_ssh_connection}    Open ONOS SSH Connection    ${ONOS_SSH_IP}    ${ONOS_SSH_PORT}
+    Set Suite Variable  ${onos_ssh_connection}
+    ${switch_type}=    Get Variable Value    ${web_power_switch.type}
+    Run Keyword If  "${switch_type}"!=""    Set Global Variable    ${powerswitch_type}    ${switch_type}
+    # start port forwarding if needed (when voltha runs in k8s)
+    ${portFwdHandle} =    Run Keyword If    ${PORT_FORWARDING}    Start Process
+    ...    kubectl port-forward --address 0.0.0.0 --namespace default svc/kafka-0-external ${KAFKA_PORT}:${KAFKA_SVC_PORT} &
+    ...    shell=true
+    Set Suite Variable   ${portFwdHandle}
+    Sleep    5s
+    # open connection to read kafka bus
+    Wait Until Keyword Succeeds     3x    5s
+    ...    kafka.Connection Open    ${KAFKA_IP}    ${KAFKA_PORT}    voltha.events    timestamp_from=0
+    # enable OLT(s) and bring up ONU(s)
+    Setup
+    Run Keyword If    "${workflow}"=="DT"    Perform Sanity Test DT
+    ...    ELSE IF    "${workflow}"=="TT"    Perform Sanity Tests TT
+    ...    ELSE       Perform Sanity Test
+    # prepare pm data matrix for validation
+    ${METRIC_DICT}=     Create Metric Dictionary
+    Set Suite Variable    ${METRIC_DICT}
+
+Teardown Suite
+    [Documentation]    tear down the test suite
+    # close connection to kafka
+    kafka.Connection Close
+    # stop port forwarding if started
+    Run Keyword If    ${PORT_FORWARDING}    Terminate Process    ${portFwdHandle}    kill=true
+    # call common suite teardown
+    utils.Teardown Suite
+    Close All ONOS SSH Connections