/*
* ============================================================================
* GNU General Public License
* ============================================================================
*
* Copyright (C) 2006-2011 Serotonin Software Technologies Inc. http://serotoninsoftware.com
* @author Matthew Lohbihler
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* When signing a commercial license with Serotonin Software Technologies Inc.,
* the following extension to GPL is made. A special exception to the GPL is
* included to allow you to distribute a combined work that includes BAcnet4J
* without being obliged to provide the source code for any proprietary components.
*/
package com.serotonin.bacnet4j.obj;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.serotonin.bacnet4j.exception.BACnetServiceException;
import com.serotonin.bacnet4j.type.Encodable;
import com.serotonin.bacnet4j.type.constructed.Address;
import com.serotonin.bacnet4j.type.constructed.PropertyValue;
import com.serotonin.bacnet4j.type.enumerated.ObjectType;
import com.serotonin.bacnet4j.type.enumerated.PropertyIdentifier;
import com.serotonin.bacnet4j.type.primitive.OctetString;
import com.serotonin.bacnet4j.type.primitive.Real;
import com.serotonin.bacnet4j.type.primitive.UnsignedInteger;
public class ObjectCovSubscription implements Serializable {
private static final long serialVersionUID = 3546250271813406695L;
private static Set<ObjectType> supportedObjectTypes = new HashSet<ObjectType>();
private static Set<PropertyIdentifier> supportedPropertyIdentifiers = new HashSet<PropertyIdentifier>();
/** These types require a COV threshold, before any subscriptions are allowed */
private static Set<ObjectType> covThresholdRequired = new HashSet<ObjectType>();
static {
supportedObjectTypes.add(ObjectType.accessDoor);
supportedObjectTypes.add(ObjectType.accumulator);
supportedObjectTypes.add(ObjectType.analogInput);
supportedObjectTypes.add(ObjectType.analogOutput);
supportedObjectTypes.add(ObjectType.analogValue);
supportedObjectTypes.add(ObjectType.binaryInput);
supportedObjectTypes.add(ObjectType.binaryOutput);
supportedObjectTypes.add(ObjectType.binaryValue);
supportedObjectTypes.add(ObjectType.lifeSafetyPoint);
supportedObjectTypes.add(ObjectType.loop);
supportedObjectTypes.add(ObjectType.multiStateInput);
supportedObjectTypes.add(ObjectType.multiStateOutput);
supportedObjectTypes.add(ObjectType.multiStateValue);
supportedObjectTypes.add(ObjectType.pulseConverter);
supportedPropertyIdentifiers.add(PropertyIdentifier.presentValue);
supportedPropertyIdentifiers.add(PropertyIdentifier.statusFlags);
supportedPropertyIdentifiers.add(PropertyIdentifier.doorAlarmState);
covThresholdRequired.add(ObjectType.analogInput);
covThresholdRequired.add(ObjectType.analogOutput);
covThresholdRequired.add(ObjectType.analogValue);
covThresholdRequired.add(ObjectType.loop);
covThresholdRequired.add(ObjectType.pulseConverter);
}
public static void addSupportedObjectType(ObjectType objectType) {
supportedObjectTypes.add(objectType);
}
public static void addSupportedPropertyIdentifier(PropertyIdentifier propertyIdentifier) {
supportedPropertyIdentifiers.add(propertyIdentifier);
}
public static boolean supportedObjectType(ObjectType objectType) {
return supportedObjectTypes.contains(objectType);
}
public static boolean sendCovNotification(ObjectType objectType, PropertyIdentifier pid, Real covThresholdValue) {
if (!supportedObjectType(objectType))
return false;
if (pid != null && !supportedPropertyIdentifiers.contains(pid))
return false;
// Don't allow COV notifications when there is no threshold for Objects that require thresholds.
if (covThresholdRequired.contains(objectType) && covThresholdValue == null)
return false;
return true;
}
public static List<PropertyValue> getValues(BACnetObject obj) {
List<PropertyValue> values = new ArrayList<PropertyValue>();
for (PropertyIdentifier pid : supportedPropertyIdentifiers)
addValue(obj, values, pid);
return values;
}
private static void addValue(BACnetObject obj, List<PropertyValue> values, PropertyIdentifier pid) {
try {
// Ensure that the obj has the given property. The addition of doorAlarmState requires this.
if (ObjectProperties.getPropertyTypeDefinition(obj.getId().getObjectType(), pid) != null) {
Encodable value = obj.getProperty(pid);
if (value != null)
values.add(new PropertyValue(pid, value));
}
}
catch (BACnetServiceException e) {
// Should never happen, so wrap in a RuntimeException
throw new RuntimeException(e);
}
}
private final Address address;
private final OctetString linkService;
private final UnsignedInteger subscriberProcessIdentifier;
private boolean issueConfirmedNotifications;
private long expiryTime;
/**
* The increment/threshold at which COV notifications should be sent out. Only applies to property identifiers that
* are {@link Real}'s
* and {@link ObjectType}'s mentioned in {@link ObjectCovSubscription#covThresholdRequired}.
*/
private final Real covIncrement;
/**
* Contains the last sent values per property identifier. It is used to determine if a COV notification should be
* sent.
*/
private final Map<PropertyIdentifier, Encodable> lastSentValues = new HashMap<PropertyIdentifier, Encodable>();
public ObjectCovSubscription(Address address, OctetString linkService, UnsignedInteger subscriberProcessIdentifier,
Real covIncrement) {
this.address = address;
this.linkService = linkService;
this.subscriberProcessIdentifier = subscriberProcessIdentifier;
this.covIncrement = covIncrement;
}
public Address getAddress() {
return address;
}
public OctetString getLinkService() {
return linkService;
}
public boolean isIssueConfirmedNotifications() {
return issueConfirmedNotifications;
}
public UnsignedInteger getSubscriberProcessIdentifier() {
return subscriberProcessIdentifier;
}
public void setIssueConfirmedNotifications(boolean issueConfirmedNotifications) {
this.issueConfirmedNotifications = issueConfirmedNotifications;
}
public void setExpiryTime(int seconds) {
if (seconds == 0)
expiryTime = -1;
else
expiryTime = System.currentTimeMillis() + seconds * 1000;
}
public boolean hasExpired(long now) {
if (expiryTime == -1)
return false;
return expiryTime < now;
}
public int getTimeRemaining(long now) {
if (expiryTime == -1)
return 0;
int left = (int) ((expiryTime - now) / 1000);
if (left < 1)
return 1;
return left;
}
/**
* Determine if a notification needs to be sent out based on the Threshold if relevant.
*
* @param pid
* The {@link PropertyIdentifier} being updated
* @param value
* The new value
* @return true if a COV notification should be sent out, false otherwise.
*/
public boolean isNotificationRequired(PropertyIdentifier pid, Encodable value) {
Encodable lastSentValue = this.lastSentValues.get(pid);
boolean notificationRequired = ThresholdCalculator.isValueOutsideOfThreshold(this.covIncrement, lastSentValue,
value);
if (notificationRequired) {
this.lastSentValues.put(pid, value);
}
return notificationRequired;
}
/**
* Utility Class to determine whether COV thresholds/increments have been surpassed.
*
* @author japearson
*
*/
public static class ThresholdCalculator {
/**
* Convert the given encodable value to a {@link Float} if possible.
*
* @param value
* The value to attempt to convert to a {@link Float}.
* @return A {@link Float} value if the {@link Encodable} can be converted, otherwise null.
*/
private static Float convertEncodableToFloat(Encodable value) {
Float floatValue = null;
if (value instanceof Real) {
floatValue = ((Real) value).floatValue();
}
return floatValue;
}
/**
* Determine if the newValue has surpassed the threshold value compared with the original value.
* <p>
* When the originalValue is null, it is automatically assumed to be outside the threshold, because it means the
* property hasn't been seen before.
* <p>
* If any of the parameters cannot be converted to a {@link Float}, then this method returns true when the
* original and new value are not equal and false otherwise.
*
* @param threshold
* The threshold value
* @param originalValue
* The original or last sent value
* @param newValue
* The new value to check
* @return true if the new value is outside the threshold or false otherwise.
*/
public static boolean isValueOutsideOfThreshold(Real threshold, Encodable originalValue, Encodable newValue) {
Float floatThreshold = convertEncodableToFloat(threshold);
Float floatOriginal = convertEncodableToFloat(originalValue);
Float floatNewValue = convertEncodableToFloat(newValue);
// This property hasn't been seen before, so a notification is required
if (originalValue == null) {
return true;
}
// Handle types that can't do threshold comparisons
else if (floatThreshold == null || floatOriginal == null || floatNewValue == null) {
return !originalValue.equals(newValue);
}
else {
// Due to floating point maths, it's possible that where the difference should be equal to the threshold
// and not be outside the threshold actually evaluates to true due to precision errors. However since
// this threshold is calculated only for use in deciding whether to trigger a COV notification, small
// margins of error on boundary cases are acceptable.
return Math.abs(floatNewValue - floatOriginal) > floatThreshold;
}
}
}
}