/* * ============================================================================ * 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; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.ObjectUtils; import com.serotonin.bacnet4j.enums.MaxApduLength; import com.serotonin.bacnet4j.event.DeviceEventHandler; import com.serotonin.bacnet4j.exception.BACnetException; import com.serotonin.bacnet4j.exception.BACnetRuntimeException; import com.serotonin.bacnet4j.exception.BACnetServiceException; import com.serotonin.bacnet4j.npdu.NetworkIdentifier; import com.serotonin.bacnet4j.obj.BACnetObject; import com.serotonin.bacnet4j.service.acknowledgement.AcknowledgementService; import com.serotonin.bacnet4j.service.acknowledgement.ReadPropertyAck; import com.serotonin.bacnet4j.service.confirmed.ConfirmedEventNotificationRequest; import com.serotonin.bacnet4j.service.confirmed.ConfirmedRequestService; import com.serotonin.bacnet4j.service.confirmed.ReadPropertyRequest; import com.serotonin.bacnet4j.service.unconfirmed.IAmRequest; import com.serotonin.bacnet4j.service.unconfirmed.UnconfirmedEventNotificationRequest; import com.serotonin.bacnet4j.service.unconfirmed.UnconfirmedRequestService; import com.serotonin.bacnet4j.transport.Transport; import com.serotonin.bacnet4j.type.Encodable; import com.serotonin.bacnet4j.type.constructed.Address; import com.serotonin.bacnet4j.type.constructed.Destination; import com.serotonin.bacnet4j.type.constructed.EventTransitionBits; import com.serotonin.bacnet4j.type.constructed.ObjectTypesSupported; import com.serotonin.bacnet4j.type.constructed.SequenceOf; import com.serotonin.bacnet4j.type.constructed.ServicesSupported; import com.serotonin.bacnet4j.type.constructed.TimeStamp; import com.serotonin.bacnet4j.type.enumerated.DeviceStatus; import com.serotonin.bacnet4j.type.enumerated.ErrorClass; import com.serotonin.bacnet4j.type.enumerated.ErrorCode; import com.serotonin.bacnet4j.type.enumerated.EventState; import com.serotonin.bacnet4j.type.enumerated.EventType; import com.serotonin.bacnet4j.type.enumerated.NotifyType; import com.serotonin.bacnet4j.type.enumerated.ObjectType; import com.serotonin.bacnet4j.type.enumerated.PropertyIdentifier; import com.serotonin.bacnet4j.type.enumerated.Segmentation; import com.serotonin.bacnet4j.type.notificationParameters.NotificationParameters; import com.serotonin.bacnet4j.type.primitive.Boolean; import com.serotonin.bacnet4j.type.primitive.CharacterString; import com.serotonin.bacnet4j.type.primitive.ObjectIdentifier; import com.serotonin.bacnet4j.type.primitive.OctetString; import com.serotonin.bacnet4j.type.primitive.Unsigned16; import com.serotonin.bacnet4j.type.primitive.UnsignedInteger; import com.serotonin.bacnet4j.util.RequestUtils; /** * Enhancements: - default character string encoding - BIBBs (B-OWS) (services to implement) - AE-N-A - AE-ACK-A - * AE-INFO-A - AE-ESUM-A - SCHED-A - T-VMT-A - T-ATR-A - DM-DDB-A,B - DM-DOB-A,B - DM-DCC-A - DM-TS-A - DM-UTC-A - * DM-RD-A - DM-BR-A - NM-CE-A * * @author mlohbihler */ public class LocalDevice { private static final int VENDOR_ID = 236; // Serotonin Software private final Transport transport; private BACnetObject configuration; private final List<BACnetObject> localObjects = new CopyOnWriteArrayList<BACnetObject>(); private final List<RemoteDevice> remoteDevices = new CopyOnWriteArrayList<RemoteDevice>(); private boolean initialized; private ExecutorService executorService; private boolean ownsExecutorService; /** * The local password of the device. Used in the ReinitializeDeviceRequest service. */ private String password = ""; private boolean strict; // Event listeners private final DeviceEventHandler eventHandler = new DeviceEventHandler(); //private final DeviceEventHandler eventHandler = new DeviceEventAsyncHandler(); public LocalDevice(int deviceId, Transport transport) { this.transport = transport; transport.setLocalDevice(this); try { ObjectIdentifier deviceIdentifier = new ObjectIdentifier(ObjectType.device, deviceId); configuration = new BACnetObject(this, deviceIdentifier); configuration.setProperty(PropertyIdentifier.maxApduLengthAccepted, new UnsignedInteger(1476)); configuration.setProperty(PropertyIdentifier.vendorIdentifier, new Unsigned16(VENDOR_ID)); configuration.setProperty(PropertyIdentifier.vendorName, new CharacterString( "Serotonin Software Technologies, Inc.")); configuration.setProperty(PropertyIdentifier.segmentationSupported, Segmentation.segmentedBoth); SequenceOf<ObjectIdentifier> objectList = new SequenceOf<ObjectIdentifier>(); objectList.add(deviceIdentifier); configuration.setProperty(PropertyIdentifier.objectList, objectList); // Set up the supported services indicators. Remove lines as services get implemented. ServicesSupported servicesSupported = new ServicesSupported(); servicesSupported.setAll(true); servicesSupported.setAcknowledgeAlarm(false); servicesSupported.setGetAlarmSummary(false); servicesSupported.setGetEnrollmentSummary(false); servicesSupported.setAtomicReadFile(false); servicesSupported.setAtomicWriteFile(false); servicesSupported.setAddListElement(false); servicesSupported.setRemoveListElement(false); servicesSupported.setReadPropertyConditional(false); servicesSupported.setDeviceCommunicationControl(false); servicesSupported.setReinitializeDevice(false); servicesSupported.setVtOpen(false); servicesSupported.setVtClose(false); servicesSupported.setVtData(false); servicesSupported.setAuthenticate(false); servicesSupported.setRequestKey(false); servicesSupported.setTimeSynchronization(false); servicesSupported.setReadRange(false); servicesSupported.setUtcTimeSynchronization(false); servicesSupported.setLifeSafetyOperation(false); servicesSupported.setSubscribeCovProperty(false); servicesSupported.setGetEventInformation(false); configuration.setProperty(PropertyIdentifier.protocolServicesSupported, servicesSupported); // Set up the object types supported. ObjectTypesSupported objectTypesSupported = new ObjectTypesSupported(); objectTypesSupported.setAll(true); configuration.setProperty(PropertyIdentifier.protocolObjectTypesSupported, objectTypesSupported); // Set some other required values to defaults configuration.setProperty(PropertyIdentifier.objectName, new CharacterString("BACnet device")); configuration.setProperty(PropertyIdentifier.systemStatus, DeviceStatus.operational); configuration.setProperty(PropertyIdentifier.modelName, new CharacterString("BACnet4J")); configuration.setProperty(PropertyIdentifier.firmwareRevision, new CharacterString("not set")); configuration.setProperty(PropertyIdentifier.applicationSoftwareVersion, new CharacterString("1.0.1")); configuration.setProperty(PropertyIdentifier.protocolVersion, new UnsignedInteger(1)); configuration.setProperty(PropertyIdentifier.protocolRevision, new UnsignedInteger(0)); configuration.setProperty(PropertyIdentifier.databaseRevision, new UnsignedInteger(0)); } catch (BACnetServiceException e) { // Should never happen, but wrap in an unchecked just in case. throw new BACnetRuntimeException(e); } } public ExecutorService getExecutorService() { return executorService; } public void setExecutorService(ExecutorService executorService) { if (initialized) throw new IllegalStateException("Cannot set the executor service. Already initialized"); this.executorService = executorService; } public NetworkIdentifier getNetworkIdentifier() { return transport.getNetworkIdentifier(); } /** * @return the strict */ public boolean isStrict() { return strict; } /** * @param strict * the strict to set */ public void setStrict(boolean strict) { this.strict = strict; } public synchronized void initialize() throws Exception { if (executorService == null) { executorService = Executors.newCachedThreadPool(); ownsExecutorService = true; } else ownsExecutorService = false; // For the async handler //eventHandler.initialize(executorService); transport.initialize(); initialized = true; } public synchronized void terminate() { transport.terminate(); initialized = false; if (ownsExecutorService) { ExecutorService temp = executorService; executorService = null; if (temp != null) { temp.shutdown(); try { temp.awaitTermination(3, TimeUnit.SECONDS); } catch (InterruptedException e) { // no op } } } } public boolean isInitialized() { return initialized; } public BACnetObject getConfiguration() { return configuration; } public DeviceEventHandler getEventHandler() { return eventHandler; } // // // Device configuration. // public String getPassword() { return password; } public void setPassword(String password) { if (password == null) password = ""; this.password = password; } // // // Local object management // public BACnetObject getObjectRequired(ObjectIdentifier id) throws BACnetServiceException { BACnetObject o = getObject(id); if (o == null) throw new BACnetServiceException(ErrorClass.object, ErrorCode.unknownObject); return o; } public List<BACnetObject> getLocalObjects() { return localObjects; } public BACnetObject getObject(ObjectIdentifier id) { if (id.getObjectType().intValue() == ObjectType.device.intValue()) { // Check if we need to look into the local device. if (id.getInstanceNumber() == 0x3FFFFF || id.getInstanceNumber() == configuration.getInstanceId()) return configuration; } for (BACnetObject obj : localObjects) { if (obj.getId().equals(id)) return obj; } return null; } public BACnetObject getObject(String name) { // Check if we need to look into the local device. if (name.equals(configuration.getObjectName())) return configuration; for (BACnetObject obj : localObjects) { if (name.equals(obj.getObjectName())) return obj; } return null; } public void addObject(BACnetObject obj) throws BACnetServiceException { if (getObject(obj.getId()) != null) throw new BACnetServiceException(ErrorClass.object, ErrorCode.objectIdentifierAlreadyExists); if (getObject(obj.getObjectName()) != null) throw new BACnetServiceException(ErrorClass.object, ErrorCode.duplicateName); obj.validate(); localObjects.add(obj); // Create a reference in the device's object list for the new object. getObjectList().add(obj.getId()); } public ObjectIdentifier getNextInstanceObjectIdentifier(ObjectType objectType) { // Make a list of existing ids. List<Integer> ids = new ArrayList<Integer>(); int type = objectType.intValue(); ObjectIdentifier id; for (BACnetObject obj : localObjects) { id = obj.getId(); if (id.getObjectType().intValue() == type) ids.add(id.getInstanceNumber()); } // Sort the list. Collections.sort(ids); // Find the first hole in the list. int i = 0; for (; i < ids.size(); i++) { if (ids.get(i) != i) break; } return new ObjectIdentifier(objectType, i); } public void removeObject(ObjectIdentifier id) throws BACnetServiceException { BACnetObject obj = getObject(id); if (obj != null) localObjects.remove(obj); else throw new BACnetServiceException(ErrorClass.object, ErrorCode.unknownObject); // Remove the reference in the device's object list for this id. getObjectList().remove(id); } @SuppressWarnings("unchecked") private SequenceOf<ObjectIdentifier> getObjectList() { try { return (SequenceOf<ObjectIdentifier>) configuration.getProperty(PropertyIdentifier.objectList); } catch (BACnetServiceException e) { // Should never happen, so just wrap in a RuntimeException throw new RuntimeException(e); } } public ServicesSupported getServicesSupported() throws BACnetServiceException { return (ServicesSupported) getConfiguration().getProperty(PropertyIdentifier.protocolServicesSupported); } // // // Message sending // public AcknowledgementService send(RemoteDevice d, ConfirmedRequestService serviceRequest) throws BACnetException { return transport.send(d.getAddress(), d.getLinkService(), d.getMaxAPDULengthAccepted(), d.getSegmentationSupported(), serviceRequest); } public AcknowledgementService send(Address address, MaxApduLength maxAPDULength, Segmentation segmentationSupported, ConfirmedRequestService serviceRequest) throws BACnetException { return transport.send(address, null, maxAPDULength.getMaxLength(), segmentationSupported, serviceRequest); } public AcknowledgementService send(Address address, OctetString linkService, MaxApduLength maxAPDULength, Segmentation segmentationSupported, ConfirmedRequestService serviceRequest) throws BACnetException { return transport .send(address, linkService, maxAPDULength.getMaxLength(), segmentationSupported, serviceRequest); } public void sendUnconfirmed(Address address, UnconfirmedRequestService serviceRequest) throws BACnetException { transport.sendUnconfirmed(address, null, serviceRequest, false); } public void sendUnconfirmed(Address address, OctetString linkService, UnconfirmedRequestService serviceRequest) throws BACnetException { transport.sendUnconfirmed(address, linkService, serviceRequest, false); } public void sendLocalBroadcast(UnconfirmedRequestService serviceRequest) throws BACnetException { Address bcast = transport.getLocalBroadcastAddress(); transport.sendUnconfirmed(bcast, null, serviceRequest, true); } public void sendGlobalBroadcast(UnconfirmedRequestService serviceRequest) throws BACnetException { transport.sendUnconfirmed(Address.GLOBAL, null, serviceRequest, true); } public void sendBroadcast(Address address, OctetString linkService, UnconfirmedRequestService serviceRequest) throws BACnetException { transport.sendUnconfirmed(address, linkService, serviceRequest, true); } // // // Remote device management // public RemoteDevice getRemoteDevice(int instanceId) throws BACnetException { RemoteDevice d = getRemoteDeviceImpl(instanceId, null, null); if (d == null) throw new BACnetException("Unknown device: instance id=" + instanceId); return d; } public RemoteDevice getRemoteDevice(int instanceId, Address address, OctetString linkService) throws BACnetException { RemoteDevice d = getRemoteDeviceImpl(instanceId, address, linkService); if (d == null) throw new BACnetException("Unknown device: instance id=" + instanceId + ", address=" + address + ", linkService=" + linkService); return d; } public RemoteDevice getRemoteDeviceCreate(int instanceId, Address address, OctetString linkService) { RemoteDevice d = getRemoteDeviceImpl(instanceId, address, linkService); if (d == null) { if (address == null) throw new NullPointerException("addr cannot be null"); d = new RemoteDevice(instanceId, address, linkService); remoteDevices.add(d); } return d; } public void addRemoteDevice(RemoteDevice d) { remoteDevices.add(d); } private RemoteDevice getRemoteDeviceImpl(int instanceId, Address address, OctetString linkService) { for (RemoteDevice d : remoteDevices) { if (strict || address == null) { // Only compare by device id, as should be sufficient according to the spec's insistence on // unique device ids. if (d.getInstanceNumber() == instanceId) return d; } else { // Compare device ids and address. if (d.getInstanceNumber() == instanceId && d.getAddress().equals(address) && ObjectUtils.equals(d.getLinkService(), linkService)) return d; } } return null; } public List<RemoteDevice> getRemoteDevices() { return remoteDevices; } public RemoteDevice getRemoteDevice(Address address) { for (RemoteDevice d : remoteDevices) { if (d.getAddress().equals(address)) return d; } return null; } public RemoteDevice getRemoteDeviceByUserData(Object userData) { for (RemoteDevice d : remoteDevices) { if (ObjectUtils.equals(userData, d.getUserData())) return d; } return null; } // // // Intrinsic events // @SuppressWarnings("unchecked") public List<BACnetException> sendIntrinsicEvent(ObjectIdentifier eventObjectIdentifier, TimeStamp timeStamp, int notificationClassId, EventType eventType, CharacterString messageText, NotifyType notifyType, EventState fromState, EventState toState, NotificationParameters eventValues) throws BACnetException { // Try to find a notification class with the given id in the local objects. BACnetObject nc = null; for (BACnetObject obj : localObjects) { if (ObjectType.notificationClass.equals(obj.getId().getObjectType())) { try { UnsignedInteger ncId = (UnsignedInteger) obj.getProperty(PropertyIdentifier.notificationClass); if (ncId != null && ncId.intValue() == notificationClassId) { nc = obj; break; } } catch (BACnetServiceException e) { // Should never happen, so wrap in a RTE throw new RuntimeException(e); } } } if (nc == null) throw new BACnetException("Notification class object not found for given id: " + notificationClassId); // Get the required properties from the notification class object. SequenceOf<Destination> recipientList = null; Boolean ackRequired = null; UnsignedInteger priority = null; try { recipientList = (SequenceOf<Destination>) nc.getPropertyRequired(PropertyIdentifier.recipientList); ackRequired = new Boolean( ((EventTransitionBits) nc.getPropertyRequired(PropertyIdentifier.ackRequired)).contains(toState)); // Determine which priority value to use based upon the toState. SequenceOf<UnsignedInteger> priorities = (SequenceOf<UnsignedInteger>) nc .getPropertyRequired(PropertyIdentifier.priority); if (toState.equals(EventState.normal)) priority = priorities.get(3); else if (toState.equals(EventState.fault)) priority = priorities.get(2); else // everything else is offnormal priority = priorities.get(1); } catch (BACnetServiceException e) { // Should never happen, so wrap in a RTE throw new RuntimeException(e); } // Send the message to the destinations that are interested in it, while recording any exceptions in the result // list List<BACnetException> sendExceptions = new ArrayList<BACnetException>(); for (Destination destination : recipientList) { if (destination.isSuitableForEvent(timeStamp, toState)) { if (destination.getIssueConfirmedNotifications().booleanValue()) { RemoteDevice remoteDevice = null; if (destination.getRecipient().isAddress()) remoteDevice = getRemoteDevice(destination.getRecipient().getAddress()); else remoteDevice = getRemoteDevice(destination.getRecipient().getObjectIdentifier() .getInstanceNumber()); if (remoteDevice != null) { ConfirmedEventNotificationRequest req = new ConfirmedEventNotificationRequest( destination.getProcessIdentifier(), configuration.getId(), eventObjectIdentifier, timeStamp, new UnsignedInteger(notificationClassId), priority, eventType, messageText, notifyType, ackRequired, fromState, toState, eventValues); try { send(remoteDevice, req); } catch (BACnetException e) { sendExceptions.add(e); } } } else { Address address = null; if (destination.getRecipient().isAddress()) address = destination.getRecipient().getAddress(); else { RemoteDevice remoteDevice = getRemoteDevice(destination.getRecipient().getObjectIdentifier() .getInstanceNumber()); if (remoteDevice != null) address = remoteDevice.getAddress(); } if (address != null) { UnconfirmedEventNotificationRequest req = new UnconfirmedEventNotificationRequest( destination.getProcessIdentifier(), configuration.getId(), eventObjectIdentifier, timeStamp, new UnsignedInteger(notificationClassId), priority, eventType, messageText, notifyType, ackRequired, fromState, toState, eventValues); try { transport.sendUnconfirmed(address, null, req, false); } catch (BACnetException e) { sendExceptions.add(e); } } } } } return sendExceptions; } // // // Convenience methods // public Address[] getAllLocalAddresses() { return transport.getNetwork().getAllLocalAddresses(); } public IAmRequest getIAm() { try { return new IAmRequest(configuration.getId(), (UnsignedInteger) configuration.getProperty(PropertyIdentifier.maxApduLengthAccepted), (Segmentation) configuration.getProperty(PropertyIdentifier.segmentationSupported), (Unsigned16) configuration.getProperty(PropertyIdentifier.vendorIdentifier)); } catch (BACnetServiceException e) { // Should never happen, so just wrap in a RuntimeException throw new RuntimeException(e); } } // // Manual device discovery public RemoteDevice findRemoteDevice(Address address, OctetString linkService, int deviceId) throws BACnetException { RemoteDevice d = getRemoteDeviceImpl(deviceId, address, linkService); if (d != null) return d; ObjectIdentifier deviceOid = new ObjectIdentifier(ObjectType.device, deviceId); ReadPropertyRequest req = new ReadPropertyRequest(deviceOid, PropertyIdentifier.maxApduLengthAccepted); ReadPropertyAck ack = (ReadPropertyAck) transport.send(address, linkService, MaxApduLength.UP_TO_50.getMaxLength(), Segmentation.noSegmentation, req); // If we got this far, then we got a response. Now get the other required properties. d = new RemoteDevice(deviceOid.getInstanceNumber(), address, linkService); d.setMaxAPDULengthAccepted(((UnsignedInteger) ack.getValue()).intValue()); d.setSegmentationSupported(Segmentation.noSegmentation); Map<PropertyIdentifier, Encodable> map = RequestUtils.getProperties(this, d, null, PropertyIdentifier.segmentationSupported, PropertyIdentifier.vendorIdentifier, PropertyIdentifier.protocolServicesSupported); d.setSegmentationSupported((Segmentation) map.get(PropertyIdentifier.segmentationSupported)); d.setVendorId(((Unsigned16) map.get(PropertyIdentifier.vendorIdentifier)).intValue()); d.setServicesSupported((ServicesSupported) map.get(PropertyIdentifier.protocolServicesSupported)); addRemoteDevice(d); return d; } }