Skip to content

Commit

Permalink
Merge pull request OSGP#97 from OSGP/feature/FDP-2874-get-analog-alar…
Browse files Browse the repository at this point in the history
…m-thresholds-command

Feature/fdp 2874 get analog alarm thresholds command
  • Loading branch information
smvdheijden authored Feb 7, 2025
2 parents 19c58e9 + d09295d commit 934ed34
Show file tree
Hide file tree
Showing 37 changed files with 568 additions and 82 deletions.
2 changes: 2 additions & 0 deletions application/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ dependencies {

implementation(libs.commonsCodec)

implementation(libs.jacksonKotlinModule)

runtimeOnly(libs.micrometerPrometheusModule)
runtimeOnly(libs.postgresql)
runtimeOnly(libs.flyway)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Command(

enum class CommandType(val downlink: String, val needsCommandValue: Boolean = false) {
ANALOG_ALARM_THRESHOLDS("AL", needsCommandValue = true),
INFO_ALARMS("INFO:ALARMS"),
PSK("PSK"),
PSK_SET("PSK:SET"),
REBOOT("CMD:REBOOT"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.feedbackgenerator

import com.fasterxml.jackson.databind.JsonNode
import org.gxf.crestdeviceservice.command.entity.Command.CommandType

interface CommandFeedbackGenerator {
val supportedCommandType: CommandType

fun generateFeedback(message: JsonNode): String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.feedbackgenerator

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class CommandFeedbackGeneratorConfig {
@Bean
fun commandFeedbackGeneratorsByType(commandFeedbackGenerators: List<CommandFeedbackGenerator>) =
commandFeedbackGenerators.associateBy { it.supportedCommandType }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.feedbackgenerator

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.gxf.crestdeviceservice.command.entity.Command
import org.gxf.crestdeviceservice.command.mapper.AnalogAlarmThresholdCalculator
import org.gxf.crestdeviceservice.command.service.AlarmsInfoService
import org.springframework.stereotype.Component

@Component
class InfoAlarmsFeedbackGenerator(private val alarmsInfoService: AlarmsInfoService) : CommandFeedbackGenerator {
val mapper = jacksonObjectMapper()

override val supportedCommandType = Command.CommandType.INFO_ALARMS

override fun generateFeedback(message: JsonNode): String {
val alarmsInfo = alarmsInfoService.getAlarmsInfo(message)
val calculatedAlarmsInfo = AnalogAlarmThresholdCalculator.calculateThresholdsFromDevice(alarmsInfo)
return mapper.writeValueAsString(calculatedAlarmsInfo)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@
package org.gxf.crestdeviceservice.command.mapper

import kotlin.math.roundToInt
import org.gxf.crestdeviceservice.model.AlarmsInfo

object AnalogAlarmThresholdCalculator {
// volt = millibar / 500
// payload value = volt * 200
fun getPayloadFromMillibar(millibarValue: Int) = (millibarValue * 0.4).roundToInt()

fun calculateThresholdsFromDevice(alarmsInfo: AlarmsInfo) =
alarmsInfo.copy(
AL6 = alarmsInfo.AL6?.map { getMillibarFromPayload(it) },
AL7 = alarmsInfo.AL7?.map { getMillibarFromPayload(it) },
)

fun getMillibarFromPayload(payloadValue: Int) = (payloadValue * 2.5).roundToInt()
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.gxf.crestdeviceservice.command.service.CommandService
import org.springframework.stereotype.Component

@Component
class AnalogAlarmThresholdResultHandler(
class AnalogAlarmThresholdsResultHandler(
val commandService: CommandService,
val commandFeedbackService: CommandFeedbackService,
) : CommandResultHandler(commandService, commandFeedbackService) {
Expand All @@ -20,16 +20,16 @@ class AnalogAlarmThresholdResultHandler(

override val supportedCommandType = Command.CommandType.ANALOG_ALARM_THRESHOLDS

override fun hasSucceeded(command: Command, body: JsonNode): Boolean {
override fun hasSucceeded(command: Command, message: JsonNode): Boolean {
val channel = getChannelFromCommand(command)
val fullSuccesUrc = "$channel:$partialSuccessUrc"
return fullSuccesUrc in body.urcs()
return fullSuccesUrc in message.urcs()
}

override fun hasFailed(command: Command, body: JsonNode): Boolean {
override fun hasFailed(command: Command, message: JsonNode): Boolean {
val channel = getChannelFromCommand(command)
val fullErrorUrcs = partialErrorUrcs.map { "$channel:$it" }
return body.urcs().any { it in fullErrorUrcs }
return message.urcs().any { it in fullErrorUrcs }
}

private fun getChannelFromCommand(command: Command): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.JsonNode
import io.github.oshai.kotlinlogging.KotlinLogging
import org.gxf.crestdeviceservice.command.entity.Command
import org.gxf.crestdeviceservice.command.entity.Command.CommandType
import org.gxf.crestdeviceservice.command.feedbackgenerator.CommandFeedbackGenerator
import org.gxf.crestdeviceservice.command.service.CommandFeedbackService
import org.gxf.crestdeviceservice.command.service.CommandService
import org.gxf.crestdeviceservice.model.ErrorUrc.Companion.getMessageFromCode
Expand All @@ -19,39 +20,53 @@ abstract class CommandResultHandler(

abstract val supportedCommandType: CommandType

abstract fun hasSucceeded(command: Command, body: JsonNode): Boolean
abstract fun hasSucceeded(command: Command, message: JsonNode): Boolean

abstract fun hasFailed(command: Command, body: JsonNode): Boolean
abstract fun hasFailed(command: Command, message: JsonNode): Boolean

fun handleSuccess(command: Command) {
fun handleSuccess(command: Command, message: JsonNode, feedbackGenerator: CommandFeedbackGenerator? = null) {
logger.info { "Command ${command.type} succeeded for device with id ${command.deviceId}." }

handleCommandSpecificSuccess(command)
handleCommandSpecificSuccess(command, message)

logger.debug { "Saving command and sending feedback to Maki." }
val successfulCommand = commandService.saveCommand(command.finish())
commandFeedbackService.sendSuccessFeedback(successfulCommand)

sendSuccessFeedback(feedbackGenerator, message, successfulCommand)
}

private fun sendSuccessFeedback(
feedbackGenerator: CommandFeedbackGenerator?,
message: JsonNode,
successfulCommand: Command,
) {
if (feedbackGenerator == null) {
commandFeedbackService.sendSuccessFeedback(successfulCommand)
} else {
val feedback = feedbackGenerator.generateFeedback(message)
commandFeedbackService.sendSuccessFeedback(successfulCommand, feedback)
}
}

/** Override this method when custom success actions are needed. */
open fun handleCommandSpecificSuccess(command: Command) {
open fun handleCommandSpecificSuccess(command: Command, message: JsonNode) {
logger.debug {
"Command ${command.type} for device with id ${command.deviceId} does not require specific success handling."
}
}

fun handleFailure(command: Command, body: JsonNode) {
fun handleFailure(command: Command, message: JsonNode) {
logger.info { "Command ${command.type} failed for device with id ${command.deviceId}." }

handleCommandSpecificFailure(command, body)
handleCommandSpecificFailure(command, message)

val failedCommand = commandService.saveCommand(command.fail())
val errorMessages = body.urcs().joinToString(". ") { urc -> getMessageFromCode(urc) }
val errorMessages = message.urcs().joinToString(". ") { urc -> getMessageFromCode(urc) }
commandFeedbackService.sendErrorFeedback(failedCommand, "Command failed. Error(s): $errorMessages.")
}

/** Override this method when command specific failure actions are needed */
open fun handleCommandSpecificFailure(command: Command, body: JsonNode) {
open fun handleCommandSpecificFailure(command: Command, message: JsonNode) {
logger.debug {
"Command ${command.type} for device with id ${command.deviceId} does not require specific failure handling."
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class FirmwareCommandResultHandler(

override val supportedCommandType = CommandType.FIRMWARE

override fun hasSucceeded(command: Command, body: JsonNode) = successUrc in body.urcs()
override fun hasSucceeded(command: Command, message: JsonNode) = successUrc in message.urcs()

override fun hasFailed(command: Command, body: JsonNode) = body.urcs().any { it in errorUrcs }
override fun hasFailed(command: Command, message: JsonNode) = message.urcs().any { it in errorUrcs }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.resulthandler

import com.fasterxml.jackson.databind.JsonNode
import org.gxf.crestdeviceservice.command.entity.Command
import org.gxf.crestdeviceservice.command.service.AlarmsInfoService
import org.gxf.crestdeviceservice.command.service.CommandFeedbackService
import org.gxf.crestdeviceservice.command.service.CommandService
import org.springframework.stereotype.Component

@Component
class InfoAlarmsResultHandler(
commandService: CommandService,
commandFeedbackService: CommandFeedbackService,
private val alarmsInfoService: AlarmsInfoService,
) : CommandResultHandler(commandService, commandFeedbackService) {
private val errorUrcs = listOf("INFO:DLER", "INFO:ERR", "INFO:DLNA")

override val supportedCommandType = Command.CommandType.INFO_ALARMS

override fun hasSucceeded(command: Command, message: JsonNode) =
try {
alarmsInfoService.getAlarmsInfo(message)
true
} catch (_: Exception) {
false
}

override fun hasFailed(command: Command, message: JsonNode): Boolean = message.urcs().any { it in errorUrcs }
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class PskCommandResultHandler(commandService: CommandService, commandFeedbackSer

override val supportedCommandType = CommandType.PSK

override fun hasSucceeded(command: Command, body: JsonNode) = successUrc in body.urcs()
override fun hasSucceeded(command: Command, message: JsonNode) = successUrc in message.urcs()

override fun hasFailed(command: Command, body: JsonNode) = body.urcs().any { it in errorUrcs }
override fun hasFailed(command: Command, message: JsonNode) = message.urcs().any { it in errorUrcs }
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@ class PskSetCommandResultHandler(

override val supportedCommandType = CommandType.PSK_SET

override fun hasSucceeded(command: Command, body: JsonNode) = successUrc in body.urcs()
override fun hasSucceeded(command: Command, message: JsonNode) = successUrc in message.urcs()

override fun hasFailed(command: Command, body: JsonNode) = body.urcs().any { it in errorUrcs }
override fun hasFailed(command: Command, message: JsonNode) = message.urcs().any { it in errorUrcs }

override fun handleCommandSpecificSuccess(command: Command) {
override fun handleCommandSpecificSuccess(command: Command, message: JsonNode) {
logger.info { "PSK SET command succeeded: Changing active key for device ${command.deviceId}" }
pskService.changeActiveKey(command.deviceId)
}

override fun handleCommandSpecificFailure(command: Command, body: JsonNode) {
override fun handleCommandSpecificFailure(command: Command, message: JsonNode) {
logger.info { "PSK SET command failed: Setting pending key as invalid for device ${command.deviceId}" }
pskService.setPendingKeyAsInvalid(command.deviceId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class RebootCommandResultHandler(commandService: CommandService, commandFeedback

override val supportedCommandType = CommandType.REBOOT

override fun hasSucceeded(command: Command, body: JsonNode) = successUrc in body.urcs()
override fun hasSucceeded(command: Command, message: JsonNode) = successUrc in message.urcs()

override fun hasFailed(command: Command, body: JsonNode) = false
override fun hasFailed(command: Command, message: JsonNode) = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class RspCommandResultHandler(commandService: CommandService, commandFeedbackSer

override val supportedCommandType = CommandType.RSP

override fun hasSucceeded(command: Command, body: JsonNode) = successUrc in body.urcs()
override fun hasSucceeded(command: Command, message: JsonNode) = successUrc in message.urcs()

override fun hasFailed(command: Command, body: JsonNode) = errorUrc in body.urcs()
override fun hasFailed(command: Command, message: JsonNode) = errorUrc in message.urcs()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.service

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.gxf.crestdeviceservice.command.entity.Command
import org.gxf.crestdeviceservice.command.resulthandler.CommandResultHandler.Companion.downlinks
import org.gxf.crestdeviceservice.model.AlarmsInfo
import org.springframework.stereotype.Service

@Service
class AlarmsInfoService {
private val mapper = jacksonObjectMapper()

fun getAlarmsInfo(message: JsonNode): AlarmsInfo {
val downlink = message.downlinks().first { it.contains(Command.CommandType.INFO_ALARMS.downlink) }
val json = downlink.substringAfter(", ")

return mapper.readValue<AlarmsInfo>(json)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ class CommandFeedbackService(
sendFeedback(commandFeedback)
}

fun sendSuccessFeedback(command: Command) {
val commandFeedback = commandEntityToCommandFeedback(command, Successful, "Command handled successfully")
fun sendSuccessFeedback(command: Command, feedback: String = "Command handled successfully") {
val commandFeedback = commandEntityToCommandFeedback(command, Successful, feedback)
sendFeedback(commandFeedback)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,44 @@ package org.gxf.crestdeviceservice.command.service
import com.fasterxml.jackson.databind.JsonNode
import io.github.oshai.kotlinlogging.KotlinLogging
import org.gxf.crestdeviceservice.command.entity.Command
import org.gxf.crestdeviceservice.command.entity.Command.CommandType
import org.gxf.crestdeviceservice.command.exception.NoCommandResultHandlerForCommandTypeException
import org.gxf.crestdeviceservice.command.feedbackgenerator.CommandFeedbackGenerator
import org.gxf.crestdeviceservice.command.resulthandler.CommandResultHandler
import org.springframework.stereotype.Service

@Service
class CommandResultService(
private val commandService: CommandService,
private val commandResultHandlersByType: Map<Command.CommandType, CommandResultHandler>,
private val commandResultHandlersByType: Map<CommandType, CommandResultHandler>,
private val commandFeedbackGenerators: List<CommandFeedbackGenerator>,
) {
private val logger = KotlinLogging.logger {}

fun handleMessage(deviceId: String, body: JsonNode) {
fun handleMessage(deviceId: String, message: JsonNode) {
val commandsInProgress = commandService.getAllCommandsInProgressForDevice(deviceId)

commandsInProgress.forEach { checkResult(it, body) }
commandsInProgress.forEach { checkResult(it, message) }
}

private fun checkResult(command: Command, body: JsonNode) {
private fun checkResult(command: Command, message: JsonNode) {
logger.debug { "Checking result for pending command of type ${command.type} for device ${command.deviceId}" }

val resultHandler =
commandResultHandlersByType[command.type]
?: throw NoCommandResultHandlerForCommandTypeException(
"No command result handler for command type ${command.type}"
)
handleResult(command, resultHandler, message)
}

private fun handleResult(command: Command, resultHandler: CommandResultHandler, message: JsonNode) {
val feedbackGenerator = commandFeedbackGenerators.firstOrNull { it.supportedCommandType == command.type }

when {
resultHandler.hasSucceeded(command, body) -> resultHandler.handleSuccess(command)
resultHandler.hasFailed(command, body) -> resultHandler.handleFailure(command, body)
resultHandler.hasSucceeded(command, message) ->
resultHandler.handleSuccess(command, message, feedbackGenerator)
resultHandler.hasFailed(command, message) -> resultHandler.handleFailure(command, message)
else -> resultHandler.handleStillInProgress(command)
}
}
Expand Down
Loading

0 comments on commit 934ed34

Please sign in to comment.