/*
* ============================================================================
* 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.List;
import java.util.Map;
import org.apache.commons.lang3.ObjectUtils;
import com.serotonin.bacnet4j.LocalDevice;
import com.serotonin.bacnet4j.RemoteDevice;
import com.serotonin.bacnet4j.event.ExceptionDispatch;
import com.serotonin.bacnet4j.exception.BACnetException;
import com.serotonin.bacnet4j.exception.BACnetRuntimeException;
import com.serotonin.bacnet4j.exception.BACnetServiceException;
import com.serotonin.bacnet4j.service.confirmed.ConfirmedCovNotificationRequest;
import com.serotonin.bacnet4j.service.unconfirmed.UnconfirmedCovNotificationRequest;
import com.serotonin.bacnet4j.type.Encodable;
import com.serotonin.bacnet4j.type.constructed.Address;
import com.serotonin.bacnet4j.type.constructed.BaseType;
import com.serotonin.bacnet4j.type.constructed.PriorityArray;
import com.serotonin.bacnet4j.type.constructed.PriorityValue;
import com.serotonin.bacnet4j.type.constructed.PropertyValue;
import com.serotonin.bacnet4j.type.constructed.SequenceOf;
import com.serotonin.bacnet4j.type.enumerated.BinaryPV;
import com.serotonin.bacnet4j.type.enumerated.ErrorClass;
import com.serotonin.bacnet4j.type.enumerated.ErrorCode;
import com.serotonin.bacnet4j.type.enumerated.ObjectType;
import com.serotonin.bacnet4j.type.enumerated.PropertyIdentifier;
import com.serotonin.bacnet4j.type.primitive.Boolean;
import com.serotonin.bacnet4j.type.primitive.CharacterString;
import com.serotonin.bacnet4j.type.primitive.Date;
import com.serotonin.bacnet4j.type.primitive.Null;
import com.serotonin.bacnet4j.type.primitive.ObjectIdentifier;
import com.serotonin.bacnet4j.type.primitive.OctetString;
import com.serotonin.bacnet4j.type.primitive.Real;
import com.serotonin.bacnet4j.type.primitive.Time;
import com.serotonin.bacnet4j.type.primitive.UnsignedInteger;
/**
* Additional validation - all object name values must be unique. - all object id values must be unique.
*
* @author x
*
*/
public class BACnetObject implements Serializable {
private static final long serialVersionUID = 569892306207282576L;
private final LocalDevice localDevice;
private final ObjectIdentifier id;
private final Map<PropertyIdentifier, Encodable> properties = new HashMap<PropertyIdentifier, Encodable>();
private final List<ObjectCovSubscription> covSubscriptions = new ArrayList<ObjectCovSubscription>();
public BACnetObject(LocalDevice localDevice, ObjectIdentifier id) {
this.localDevice = localDevice;
if (id == null)
throw new IllegalArgumentException("object id cannot be null");
this.id = id;
try {
setProperty(PropertyIdentifier.objectName, new CharacterString(id.toString()));
// Set any default values.
List<PropertyTypeDefinition> defs = ObjectProperties.getPropertyTypeDefinitions(id.getObjectType());
for (PropertyTypeDefinition def : defs) {
if (def.getDefaultValue() != null)
setProperty(def.getPropertyIdentifier(), def.getDefaultValue());
}
}
catch (BACnetServiceException e) {
// Should never happen, but wrap in an unchecked just in case.
throw new BACnetRuntimeException(e);
}
}
public ObjectIdentifier getId() {
return id;
}
public int getInstanceId() {
return id.getInstanceNumber();
}
public String getObjectName() {
CharacterString name = getRawObjectName();
if (name == null)
return null;
return name.getValue();
}
public CharacterString getRawObjectName() {
return (CharacterString) properties.get(PropertyIdentifier.objectName);
}
public String getDescription() {
CharacterString name = (CharacterString) properties.get(PropertyIdentifier.description);
if (name == null)
return null;
return name.getValue();
}
//
//
// Get property
//
public Encodable getProperty(PropertyIdentifier pid) throws BACnetServiceException {
if (pid.intValue() == PropertyIdentifier.objectIdentifier.intValue())
return id;
if (pid.intValue() == PropertyIdentifier.objectType.intValue())
return id.getObjectType();
// Check that the requested property is valid for the object. This will throw an exception if the
// property doesn't belong.
ObjectProperties.getPropertyTypeDefinitionRequired(id.getObjectType(), pid);
// Do some property-specific checking here.
if (pid.intValue() == PropertyIdentifier.localTime.intValue())
return new Time();
if (pid.intValue() == PropertyIdentifier.localDate.intValue())
return new Date();
return properties.get(pid);
}
public Encodable getPropertyRequired(PropertyIdentifier pid) throws BACnetServiceException {
Encodable p = getProperty(pid);
if (p == null)
throw new BACnetServiceException(ErrorClass.property, ErrorCode.unknownProperty);
return p;
}
public Encodable getProperty(PropertyIdentifier pid, UnsignedInteger propertyArrayIndex)
throws BACnetServiceException {
Encodable result = getProperty(pid);
if (propertyArrayIndex == null)
return result;
if (!(result instanceof SequenceOf<?>))
throw new BACnetServiceException(ErrorClass.property, ErrorCode.propertyIsNotAnArray);
SequenceOf<?> array = (SequenceOf<?>) result;
int index = propertyArrayIndex.intValue();
if (index == 0)
return new UnsignedInteger(array.getCount());
if (index > array.getCount())
throw new BACnetServiceException(ErrorClass.property, ErrorCode.invalidArrayIndex);
return array.get(index);
}
public Encodable getPropertyRequired(PropertyIdentifier pid, UnsignedInteger propertyArrayIndex)
throws BACnetServiceException {
Encodable p = getProperty(pid, propertyArrayIndex);
if (p == null)
throw new BACnetServiceException(ErrorClass.property, ErrorCode.unknownProperty);
return p;
}
//
//
// Set property
//
public void setProperty(PropertyIdentifier pid, Encodable value) throws BACnetServiceException {
ObjectProperties.validateValue(id.getObjectType(), pid, value);
setPropertyImpl(pid, value);
// If the relinquish default was set, make sure the present value gets updated as necessary.
if (pid.equals(PropertyIdentifier.relinquishDefault))
setCommandableImpl((PriorityArray) getProperty(PropertyIdentifier.priorityArray));
}
@SuppressWarnings("unchecked")
public void setProperty(PropertyIdentifier pid, int indexBase1, Encodable value) throws BACnetServiceException {
ObjectProperties.validateSequenceValue(id.getObjectType(), pid, value);
SequenceOf<Encodable> list = (SequenceOf<Encodable>) properties.get(pid);
if (list == null) {
list = new SequenceOf<Encodable>();
setPropertyImpl(pid, list);
}
list.set(indexBase1, value);
}
public void setProperty(PropertyValue value) throws BACnetServiceException {
PropertyIdentifier pid = value.getPropertyIdentifier();
if (pid.intValue() == PropertyIdentifier.objectIdentifier.intValue())
throw new BACnetServiceException(ErrorClass.property, ErrorCode.writeAccessDenied);
if (pid.intValue() == PropertyIdentifier.objectType.intValue())
throw new BACnetServiceException(ErrorClass.property, ErrorCode.writeAccessDenied);
if (pid.intValue() == PropertyIdentifier.priorityArray.intValue())
throw new BACnetServiceException(ErrorClass.property, ErrorCode.writeAccessDenied);
// if (pid.intValue() == PropertyIdentifier.relinquishDefault.intValue())
// throw new BACnetServiceException(ErrorClass.property, ErrorCode.writeAccessDenied);
if (ObjectProperties.isCommandable((ObjectType) getProperty(PropertyIdentifier.objectType), pid))
setCommandable(value.getValue(), value.getPriority());
else if (value.getValue() == null) {
if (value.getPropertyArrayIndex() == null)
removeProperty(value.getPropertyIdentifier());
else
removeProperty(value.getPropertyIdentifier(), value.getPropertyArrayIndex());
}
else {
if (value.getPropertyArrayIndex() != null)
setProperty(pid, value.getPropertyArrayIndex().intValue(), value.getValue());
else
setProperty(pid, value.getValue());
}
}
public void setCommandable(Encodable value, UnsignedInteger priority) throws BACnetServiceException {
int pri = 16;
if (priority != null)
pri = priority.intValue();
PriorityArray priorityArray = (PriorityArray) getProperty(PropertyIdentifier.priorityArray);
priorityArray.set(pri, createCommandValue(value));
setCommandableImpl(priorityArray);
}
private void setCommandableImpl(PriorityArray priorityArray) throws BACnetServiceException {
PriorityValue priorityValue = null;
for (PriorityValue priv : priorityArray) {
if (!priv.isNull()) {
priorityValue = priv;
break;
}
}
Encodable newValue = getProperty(PropertyIdentifier.relinquishDefault);
if (priorityValue != null)
newValue = priorityValue.getValue();
setPropertyImpl(PropertyIdentifier.presentValue, newValue);
}
private void setPropertyImpl(PropertyIdentifier pid, Encodable value) {
Encodable oldValue = properties.get(pid);
properties.put(pid, value);
if (!ObjectUtils.equals(value, oldValue)) {
// Check for subscriptions.
if (ObjectCovSubscription.sendCovNotification(id.getObjectType(), pid, this.getCovIncrement())) {
synchronized (covSubscriptions) {
long now = System.currentTimeMillis();
ObjectCovSubscription sub;
for (int i = covSubscriptions.size() - 1; i >= 0; i--) {
sub = covSubscriptions.get(i);
if (sub.hasExpired(now))
covSubscriptions.remove(i);
else if (sub.isNotificationRequired(pid, value))
sendCovNotification(sub, now);
}
}
}
}
}
private PriorityValue createCommandValue(Encodable value) throws BACnetServiceException {
if (value instanceof Null)
return new PriorityValue((Null) value);
ObjectType type = (ObjectType) getProperty(PropertyIdentifier.objectType);
if (type.equals(ObjectType.accessDoor))
return new PriorityValue((BaseType) value);
if (type.equals(ObjectType.analogOutput) || type.equals(ObjectType.analogValue))
return new PriorityValue((Real) value);
if (type.equals(ObjectType.binaryOutput) || type.equals(ObjectType.binaryValue))
return new PriorityValue((BinaryPV) value);
return new PriorityValue((UnsignedInteger) value);
}
/**
* return all implemented properties
*
* @return
*/
public List<PropertyIdentifier> getProperties() {
ArrayList<PropertyIdentifier> list = new ArrayList<PropertyIdentifier>();
for (PropertyIdentifier pid : properties.keySet())
list.add(pid);
return list;
}
//
//
// COV subscriptions
//
public void addCovSubscription(Address from, OctetString linkService, UnsignedInteger subscriberProcessIdentifier,
Boolean issueConfirmedNotifications, UnsignedInteger lifetime) throws BACnetServiceException {
synchronized (covSubscriptions) {
ObjectCovSubscription sub = findCovSubscription(from, subscriberProcessIdentifier);
boolean confirmed = issueConfirmedNotifications.booleanValue();
if (sub == null) {
// Ensure that this object is valid for COV notifications.
if (!ObjectCovSubscription.sendCovNotification(id.getObjectType(), null, this.getCovIncrement()))
throw new BACnetServiceException(ErrorClass.services, ErrorCode.covSubscriptionFailed,
"Object is invalid for COV notifications");
if (confirmed) {
// If the peer wants confirmed notifications, it must be in the remote device list.
RemoteDevice d = localDevice.getRemoteDevice(from);
if (d == null)
throw new BACnetServiceException(ErrorClass.services, ErrorCode.covSubscriptionFailed,
"From address not found in remote device list. Cannot send confirmed notifications");
}
sub = new ObjectCovSubscription(from, linkService, subscriberProcessIdentifier, this.getCovIncrement());
covSubscriptions.add(sub);
}
sub.setIssueConfirmedNotifications(issueConfirmedNotifications.booleanValue());
sub.setExpiryTime(lifetime.intValue());
}
}
public Real getCovIncrement() {
return (Real) properties.get(PropertyIdentifier.covIncrement);
}
public void removeCovSubscription(Address from, UnsignedInteger subscriberProcessIdentifier) {
synchronized (covSubscriptions) {
ObjectCovSubscription sub = findCovSubscription(from, subscriberProcessIdentifier);
if (sub != null)
covSubscriptions.remove(sub);
}
}
private ObjectCovSubscription findCovSubscription(Address from, UnsignedInteger subscriberProcessIdentifier) {
for (ObjectCovSubscription sub : covSubscriptions) {
if (sub.getAddress().equals(from)
&& sub.getSubscriberProcessIdentifier().equals(subscriberProcessIdentifier))
return sub;
}
return null;
}
private void sendCovNotification(ObjectCovSubscription sub, long now) {
try {
UnsignedInteger timeLeft = new UnsignedInteger(sub.getTimeRemaining(now));
SequenceOf<PropertyValue> values = new SequenceOf<PropertyValue>(ObjectCovSubscription.getValues(this));
if (sub.isIssueConfirmedNotifications()) {
// Confirmed
ConfirmedCovNotificationRequest req = new ConfirmedCovNotificationRequest(
sub.getSubscriberProcessIdentifier(), localDevice.getConfiguration().getId(), id, timeLeft,
values);
RemoteDevice d = localDevice.getRemoteDevice(sub.getAddress());
localDevice.send(d, req);
}
else {
// Unconfirmed
UnconfirmedCovNotificationRequest req = new UnconfirmedCovNotificationRequest(
sub.getSubscriberProcessIdentifier(), localDevice.getConfiguration().getId(), id, timeLeft,
values);
localDevice.sendUnconfirmed(sub.getAddress(), sub.getLinkService(), req);
}
}
catch (BACnetException e) {
ExceptionDispatch.fireReceivedException(e);
}
}
public void validate() throws BACnetServiceException {
// Ensure that all required properties have values.
List<PropertyTypeDefinition> defs = ObjectProperties.getRequiredPropertyTypeDefinitions(id.getObjectType());
for (PropertyTypeDefinition def : defs) {
if (getProperty(def.getPropertyIdentifier()) == null)
throw new BACnetServiceException(ErrorClass.property, ErrorCode.other, "Required property not set: "
+ def.getPropertyIdentifier());
}
}
public void removeProperty(PropertyIdentifier pid) throws BACnetServiceException {
PropertyTypeDefinition def = ObjectProperties.getPropertyTypeDefinitionRequired(id.getObjectType(), pid);
if (def.isRequired())
throw new BACnetServiceException(ErrorClass.property, ErrorCode.writeAccessDenied);
properties.remove(pid);
}
public void removeProperty(PropertyIdentifier pid, UnsignedInteger propertyArrayIndex)
throws BACnetServiceException {
PropertyTypeDefinition def = ObjectProperties.getPropertyTypeDefinitionRequired(id.getObjectType(), pid);
if (!def.isSequence())
throw new BACnetServiceException(ErrorClass.property, ErrorCode.invalidArrayIndex);
SequenceOf<?> sequence = (SequenceOf<?>) properties.get(pid);
if (sequence != null)
sequence.remove(propertyArrayIndex.intValue());
}
@Override
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = PRIME * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final BACnetObject other = (BACnetObject) obj;
if (id == null) {
if (other.id != null)
return false;
}
else if (!id.equals(other.id))
return false;
return true;
}
}