# DeviceExtensionPoint::getSamplingMetrics - Help with SamplingGroupStats

**URL:** <https://forum.inductiveautomation.com/t/deviceextensionpoint-getsamplingmetrics-help-with-samplinggroupstats/110905>\
**Category:** Module Development\
**Tags:** ignition83\
**Created:** [September 23, 2025, 10:03pm UTC](https://forum.inductiveautomation.com/t/deviceextensionpoint-getsamplingmetrics-help-with-samplinggroupstats/110905 "2025-09-23T22:03:40Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [September 23, 2025, 10:03pm UTC](https://forum.inductiveautomation.com/t/deviceextensionpoint-getsamplingmetrics-help-with-samplinggroupstats/110905/1 "2025-09-23T22:03:40Z")

</div>

I'm trying to implement `DeviceExtensionPoint::getSamplingMetrics` for the first time.

I think I understand the usage of `com.codahale.metrics.Timer` and how to calculate the `AggregateStats` portion of the sampling metrics, but I'm lost on how to track the `SamplingGroupStats`.

I have an `AddressSpaceFragment` + `SubscriptionModel` that issues reads and writes to the device implementation:

```kotlin
class OidAddressSpace(val device: SnmpDeviceImpl<*>) : AddressSpaceFragment, Lifecycle {

    private val filter = SimpleAddressSpaceFilter.create { it.getPath().isOid() }
    private val subscriptionModel = SubscriptionModel(device.context.deviceContext.server, this)

    override fun startup() {
        subscriptionModel.startup()
        device.register(this)
    }

    override fun shutdown() {
        subscriptionModel.shutdown()
        device.unregister(this)
    }

    override fun read(
        context: AddressSpace.ReadContext,
        maxAge: Double,
        timestamps: TimestampsToReturn,
        readValueIds: List<ReadValueId>,
    ): List<DataValue?> {
        val results = readValueIds.map { ReadRequest(it) }
        val toProcess = results.filter { it.result == null }

        val valueReads =
            toProcess.filter {
                AttributeId.from(it.readValueId.attributeId).get() == AttributeId.Value
            }
        val valueReadResults = device.read(valueReads.map { VariableBinding(it.oid) })
        valueReadResults.zip(valueReads).forEach { (value, result) -> result.result = value }

        val nonValueReads =
            toProcess.filter {
                AttributeId.from(it.readValueId.attributeId).get() != AttributeId.Value
            }
        val nonValueReadResults = readNonValueAttributes(nonValueReads)
        nonValueReadResults.zip(nonValueReads).forEach { (value, result) -> result.result = value }

        return results.map { it.result?.value }
    }

    fun readNonValueAttributes(results: List<ReadRequest>): List<OidReadResult> {
        return results.map {
            val nodeId = it.readValueId.nodeId
            val attributeId = AttributeId.from(it.readValueId.attributeId).getOrNull()

            try {
                val result =
                    when (attributeId) {
                        AttributeId.NodeId -> nodeId

                        AttributeId.NodeClass -> NodeClass.Variable

                        AttributeId.BrowseName ->
                            device.context.deviceContext.qualifiedName(nodeId.getPath())

                        AttributeId.DisplayName,
                        AttributeId.Description -> LocalizedText.english(nodeId.getPath())

                        AttributeId.WriteMask,
                        AttributeId.UserWriteMask -> UInteger.valueOf(0)

                        AttributeId.DataType -> OpcUaDataType.String.nodeId

                        AttributeId.ValueRank -> ValueRank.Scalar.value

                        AttributeId.ArrayDimensions -> intArrayOf()

                        AttributeId.AccessLevel,
                        AttributeId.UserAccessLevel -> AccessLevel.toValue(AccessLevel.READ_WRITE)

                        AttributeId.Value ->
                            throw UaException(
                                StatusCodes.Bad_InternalError,
                                "attributeId: $attributeId",
                            )

                        else ->
                            throw UaException(
                                StatusCodes.Bad_AttributeIdInvalid,
                                "attributeId: $attributeId",
                            )
                    }!!

                OidReadResult(DataValue(Variant(result)))
            } catch (e: UaException) {
                OidReadResult(DataValue(e.statusCode))
            }
        }
    }

    override fun write(
        context: AddressSpace.WriteContext,
        writeValues: List<WriteValue>,
    ): List<StatusCode?> {
        val results = writeValues.map { WriteRequest(it) }

        results.forEach {
            if (it.writeValue.attributeId == null) {
                it.result = StatusCode(StatusCodes.Bad_AttributeIdInvalid).toOidWriteResult()
            }
            if (it.writeValue.indexRange != null && it.writeValue.indexRange.isNotEmpty()) {
                it.result = StatusCode(StatusCodes.Bad_NotImplemented).toOidWriteResult()
            }
            if (AttributeId.from(it.writeValue.attributeId).getOrNull() != AttributeId.Value) {
                it.result = StatusCode(StatusCodes.Bad_WriteNotSupported).toOidWriteResult()
            }
        }

        val valueWrites = results.filter { it.result == null }
        val valueWriteResults =
            device.write(valueWrites.map { VariableBinding(it.oid, it.value.toVariable()) })
        valueWriteResults.zip(valueWrites).forEach { (value, result) -> result.result = value }

        return results.map { it.result?.statusCode }
    }

    override fun browse(
        context: AddressSpace.BrowseContext,
        view: ViewDescription,
        nodeIds: List<NodeId>,
    ): List<AddressSpace.ReferenceResult> {
        return emptyList()
    }

    override fun gather(
        context: AddressSpace.BrowseContext,
        view: ViewDescription,
        nodeId: NodeId,
    ): AddressSpace.ReferenceResult.ReferenceList {
        return AddressSpace.ReferenceResult.ReferenceList(emptyList())
    }

    override fun onDataItemsCreated(items: List<DataItem>) {
        subscriptionModel.onDataItemsCreated(items)
    }

    override fun onDataItemsModified(items: List<DataItem>) {
        subscriptionModel.onDataItemsModified(items)
    }

    override fun onDataItemsDeleted(items: List<DataItem>) {
        subscriptionModel.onDataItemsDeleted(items)
    }

    override fun onMonitoringModeChanged(items: List<MonitoredItem>) {
        subscriptionModel.onMonitoringModeChanged(items)
    }

    override fun getFilter(): AddressSpaceFilter {
        return filter
    }

    fun NodeId.getPath(): String {
        return device.stripDeviceName(this)
    }

    inner class ReadRequest(val readValueId: ReadValueId) : OidReadRequest {
        override var result: OidReadResult? = null
        override val oid = OID(readValueId.nodeId.getPath())
    }

    inner class WriteRequest(val writeValue: WriteValue) : OidWriteRequest {
        override var result: OidWriteResult? = null
        override val value: DataValue = writeValue.value
        override val oid = OID(writeValue.nodeId.getPath())
    }
}

```

Tracking the `AggregateStats` at the device level makes sense, but how do I know what `samplingInterval` I am issuing requests for? Are there any hints contained within the `AddressSpace.ReadContext`?

---

<div class="post-metadata">

**Author:** ![Kevin.Herron](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kevin.herron/32/7259_2.png) [@Kevin.Herron](https://forum.inductiveautomation.com/u/Kevin.Herron)\
**Post date:** [September 23, 2025, 10:13pm UTC](https://forum.inductiveautomation.com/t/deviceextensionpoint-getsamplingmetrics-help-with-samplinggroupstats/110905/2 "2025-09-23T22:13:36Z")

</div>

No, you pretty much can't calculate per sampling group stats if you use the naive SubscriptionModel from Milo instead of handle MonitoredItems yourself.

All the drivers with stats implement `onDataItemCreated` etc... and have custom sampling implementations.

---

<div class="post-metadata">

**Author:** ![Kevin.Herron](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kevin.herron/32/7259_2.png) [@Kevin.Herron](https://forum.inductiveautomation.com/u/Kevin.Herron)\
**Post date:** [September 23, 2025, 10:17pm UTC](https://forum.inductiveautomation.com/t/deviceextensionpoint-getsamplingmetrics-help-with-samplinggroupstats/110905/3 "2025-09-23T22:17:24Z")

</div>

The device details / overload calculations are very much built around how we implement our drivers. It arguably does not belong in the API at all, but it was there historically and I couldn't convince myself to remove it for 8.3. I did shove it off into the extension point instead of being present on the `Device` interface, though.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [September 23, 2025, 10:26pm UTC](https://forum.inductiveautomation.com/t/deviceextensionpoint-getsamplingmetrics-help-with-samplinggroupstats/110905/4 "2025-09-23T22:26:22Z")

</div>

Cool, I might play around with a custom SubscriptionModel-type-thing that can expose what I’d need.

Or might just leave this alone and go the easy route of just implementing the Aggregate statistics…
