I would like to implement a custom QualifiedValue which contains some additional information beyond the base class of BasicQualifiedValue. However, I'm running into an issue where deserialization fails when attempting to read the config: java.util.concurrent.ExecutionException: java.lang.RuntimeException: Error cloning object through serialization.
Do I have to register the custom type with a deserializer somewhere, or something?
My custom Qualified Value looks like this (MVP):
import com.inductiveautomation.ignition.common.TypeUtilities;
import com.inductiveautomation.ignition.common.model.values.QualifiedValue;
import com.inductiveautomation.ignition.common.model.values.QualityCode;
import java.util.Date;
public class CustomQualifiedValue implements QualifiedValue {
private final Object value;
private final QualityCode quality;
private final Date timestamp;
private final Object extra;
public CustomQualifiedValue(Object value, Date timestamp) {
this.value = value;
this.quality = QualityCode.Good;
this.timestamp = timestamp;
this.extra = null;
}
public CustomQualifiedValue(Object value, QualityCode qualityCode, Date timestamp) {
this.value = value;
this.quality = qualityCode;
this.timestamp = timestamp;
this.extra = null;
}
public CustomQualifiedValue(Object value, QualityCode qualityCode, Date timestamp, Object extra) {
this.value = value;
this.quality = qualityCode;
this.timestamp = timestamp;
this.extra = extra;
}
@Override
public Object getValue() {
return value;
}
@Override
public QualityCode getQuality() {
return quality;
}
@Override
public Date getTimestamp() {
return timestamp;
}
public Object getExtra() {
return extra;
}
@Override
public boolean equals(Object val, boolean includeTimestamp) {
if (val != null && QualifiedValue.class.isAssignableFrom(val.getClass())) {
QualifiedValue other = (QualifiedValue)val;
return TypeUtilities.equals(this.quality, other.getQuality()) &&
TypeUtilities.deepEquals(this.value, other.getValue(), true) &&
(!includeTimestamp || TypeUtilities.equals(this.timestamp, other.getTimestamp()));
} else {
return false;
}
}
}
Edit: It looks like there are options for configuring serialization/deserialization via the AbstractGatewayModuleHook
(shown below) and I can add my own DeserializationHandler
, but the handler class a lot to chew on. Is there a reference implementation for the BasicQualifiedValue
or similar?
public class GatewayHook extends AbstractGatewayModuleHook {
@Override
public void configureSerializer(XMLSerializer serializer) {
super.configureSerializer(serializer);
}
@Override
public void configureDeserializer(XMLDeserializer deserializer) {
super.configureDeserializer(deserializer);
}
}
Edit: Also, I noticed that trying to create a BasicQualifiedValue with a specified timestamp doesn't actually get created with the specified timestamp. Does anyone know what the cause is of this?
Edit: It looks like I am unable to write an arbitrary timestamp to a tag. Using the test code below, both methods of writing a QualifiedValue to a tag result in the read timestamp being different than the written timestamp. It appears that even though I specify a timestamp to write in both cases, the timestamp is overwritten by whatever the current time is, or the timestamp of the last QualifiedValue is used.
// Create a dummy qualified value with timestamp and value,
// nowhere near the current time (y2k as example)
Calendar calendar = Calendar.getInstance();
calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
calendar.set(Calendar.MILLISECOND, 0);
Date timestamp = calendar.getTime();
Object value = 42;
QualifiedValue qualifiedValue = new BasicQualifiedValue(value, QualityCode.Good, timestamp);
// Write the qualified value to a tag via saveConfig, read the value,
// check whether timestamps match
tagConfig.set(WellKnownTagProps.Value, qualifiedValue);
QualityCode saveResult = _tagProvider
.saveTagConfigsAsync(Collections.singletonList(tagConfig), CollisionPolicy.Abort)
.get(30, TimeUnit.SECONDS)
.get(0);
if (saveResult.isNotGood()) {
_logger.error("Could not edit tag: " + path.toStringFull());
throw new Exception("Could not edit tag: " + path.toStringFull());
}
QualifiedValue readValue1 =
_tagProvider.readAsync(Collections.singletonList(path),
SecurityContext.emptyContext())
.get(30, TimeUnit.SECONDS).get(0);
if (readValue1.getQuality().isNotGood()) {
_logger.error("Tag value is not good: " + path.toStringFull());
throw new Exception("Tag value is not good: " + path.toStringFull());
}
var readTs1 = readValue1.getTimestamp();
if (readTs1 != timestamp) {
_logger.error("save/read timestamps do not match");
}
// Write the qualified value to a tag via writeAsync, read the value,
// check whether timestamps match
QualityCode writeResult =
_tagProvider.writeAsync(Collections.singletonList(path),
Collections.singletonList(qualifiedValue),
SecurityContext.emptyContext())
.get(30, TimeUnit.SECONDS)
.get(0);
if (writeResult.isNotGood()) {
_logger.error("Could not write value to tag: " + path.toStringFull());
throw new Exception("Could not write value to tag: " + path.toStringFull());
}
QualifiedValue readValue2 =
_tagProvider.readAsync(Collections.singletonList(path),
SecurityContext.emptyContext())
.get(30, TimeUnit.SECONDS).get(0);
if (readValue2.getQuality().isNotGood()) {
_logger.error("Tag value is not good: " + path.toStringFull());
throw new Exception("Tag value is not good: " + path.toStringFull());
}
var readTs2 = readValue2.getTimestamp();
if (readTs2 != timestamp) {
_logger.error("write/read timestamps do not match");
}
Edit: The root of what I'm trying to accomplish is to determine the 'source of a change' or 'change reason' for a tag. So, when I read a tag or receive a tag change event from a subscription, I want to determine whether my app/code/module was what changed the tag value, or something else (e.g., another module, or an end user). A custom qualified value would be strongly preferred and the 'source' could be easily determined based on the type of QualifiedValue
or a nested value, such as a change source identifier. Alternatively, if I could write arbitrary timestamps to a tag, I could keep track of the timestamp+value pairs and figure out whether my app/code/module was the source of a tag change at the cost of increased CPU + RAM consumption, which would not be preferred, and it looks like I can't write arbitrary timestamps to a tag so this isn't an option. I do not think writing and reading tag configs/properties would work because they can persist across tag value changes.
Any tips/guidance would be much appreciated.