All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.solarnetwork.node.control.bacnet.BacnetControl Maven / Gradle / Ivy

The newest version!
/* ==================================================================
 * BacnetControl.java - 10/11/2022 8:07:57 am
 * 
 * Copyright 2022 SolarNetwork.net Dev Team
 * 
 * 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 2 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, write to the Free Software 
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 
 * 02111-1307 USA
 * ==================================================================
 */

package net.solarnetwork.node.control.bacnet;

import static net.solarnetwork.service.OptionalService.service;
import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.springframework.context.MessageSource;
import net.solarnetwork.domain.BasicNodeControlInfo;
import net.solarnetwork.domain.InstructionStatus.InstructionState;
import net.solarnetwork.domain.NodeControlInfo;
import net.solarnetwork.node.domain.datum.SimpleNodeControlInfoDatum;
import net.solarnetwork.node.io.bacnet.BacnetConnection;
import net.solarnetwork.node.io.bacnet.BacnetDeviceObjectPropertyRef;
import net.solarnetwork.node.io.bacnet.BacnetNetwork;
import net.solarnetwork.node.reactor.Instruction;
import net.solarnetwork.node.reactor.InstructionHandler;
import net.solarnetwork.node.reactor.InstructionStatus;
import net.solarnetwork.node.reactor.InstructionUtils;
import net.solarnetwork.node.service.DatumEvents;
import net.solarnetwork.node.service.NodeControlProvider;
import net.solarnetwork.node.service.support.BaseIdentifiable;
import net.solarnetwork.service.FilterableService;
import net.solarnetwork.service.OptionalService;
import net.solarnetwork.settings.SettingSpecifier;
import net.solarnetwork.settings.SettingSpecifierProvider;
import net.solarnetwork.settings.SettingsChangeObserver;
import net.solarnetwork.settings.support.BasicGroupSettingSpecifier;
import net.solarnetwork.settings.support.BasicTextFieldSettingSpecifier;
import net.solarnetwork.settings.support.BasicTitleSettingSpecifier;
import net.solarnetwork.settings.support.SettingUtils;
import net.solarnetwork.util.ArrayUtils;
import net.solarnetwork.util.NumberUtils;
import net.solarnetwork.util.StringUtils;

/**
 * Control for BACnet object properties.
 * 
 * @author matt
 * @version 1.0
 */
public class BacnetControl extends BaseIdentifiable implements SettingSpecifierProvider,
		SettingsChangeObserver, NodeControlProvider, InstructionHandler {

	/** The setting UID used by this service. */
	public static final String SETTING_UID = "net.solarnetwork.node.control.bacnet";

	/** The {@code connectionCheckFrequency} property default value. */
	public static final long DEFAULT_CONNECTION_CHECK_FREQUENCY = 60_000L;

	/** The {@code reconnectDelay} property default value. */
	public static final long DEFAULT_RECONNECT_DELAY = 10_000L;

	/** The {@code sampleCacheMs} property default value. */
	public static final long DEFAULT_SAMPLE_CACHE_MS = 5_000L;

	private final OptionalService bacnetNetwork;
	private long sampleCacheMs = DEFAULT_SAMPLE_CACHE_MS;
	private BacnetWritePropertyConfig[] propConfigs;
	private OptionalService eventAdmin;

	private final ConcurrentMap propertyValues = new ConcurrentHashMap<>(8, 0.9f, 2);

	private Map propertyRefs;

	/**
	 * Constructor.
	 * 
	 * @param bacnetNetwork
	 *        the network to use
	 */
	public BacnetControl(OptionalService bacnetNetwork) {
		super();
		this.bacnetNetwork = requireNonNullArgument(bacnetNetwork, "bacnetNetwork");
	}

	@Override
	public synchronized void configurationChanged(Map properties) {
		propertyRefs = null;
	}

	@Override
	public List getAvailableControlIds() {
		BacnetWritePropertyConfig[] configs = getPropConfigs();
		if ( configs == null || configs.length < 1 ) {
			return Collections.emptyList();
		}
		return Arrays.stream(configs).filter(BacnetWritePropertyConfig::isValid)
				.map(BacnetWritePropertyConfig::getControlId).collect(Collectors.toList());
	}

	private BacnetWritePropertyConfig configForControlId(String controlId) {
		BacnetWritePropertyConfig[] configs = getPropConfigs();
		if ( controlId == null || configs == null || configs.length < 1 ) {
			return null;
		}
		for ( BacnetWritePropertyConfig config : configs ) {
			if ( controlId.equals(config.getControlId()) ) {
				return config;
			}
		}
		return null;
	}

	@Override
	public NodeControlInfo getCurrentControlInfo(String controlId) {
		BacnetWritePropertyConfig config = configForControlId(controlId);
		if ( config == null || !config.isValid() ) {
			return null;
		}
		Map values = null;
		SimpleNodeControlInfoDatum result = null;
		try (BacnetConnection conn = connection()) {
			if ( conn != null ) {
				conn.open();
				values = conn.propertyValues(propertyRefs().keySet());
				// read the control's current status
				log.debug("Reading {} value", controlId);
				try {
					Object val = values.get(config.toRef());
					result = currentValue(config, val);
					if ( result == null ) {
						propertyValues.remove(config.getControlId());
					} else {
						propertyValues.put(config.getControlId(), val);
					}
				} catch ( Exception e ) {
					log.error("Error reading {} value: {}", controlId, e.getMessage());
				}
			}
		} catch ( IOException e ) {
			log.error("Communication problem reading BACnet values for control [{}]: {}", controlId,
					e.toString());
		}
		if ( result != null ) {
			postControlEvent(result, NodeControlProvider.EVENT_TOPIC_CONTROL_INFO_CAPTURED);
		}
		return result;
	}

	private SimpleNodeControlInfoDatum newSimpleNodeControlInfoDatum(BacnetWritePropertyConfig config,
			Object value) {
		// @formatter:off
		NodeControlInfo info = BasicNodeControlInfo.builder()
				.withControlId(resolvePlaceholders(config.getControlId()))
				.withType(config.getControlPropertyType())
				.withReadonly(false)
				.withValue(value != null ? value.toString() : null)
				.build();
		// @formatter:on
		return new SimpleNodeControlInfoDatum(info, Instant.now());
	}

	private void postControlEvent(SimpleNodeControlInfoDatum info, String topic) {
		final EventAdmin admin = (eventAdmin != null ? eventAdmin.service() : null);
		if ( admin == null ) {
			return;
		}
		Event event = DatumEvents.datumEvent(topic, info);
		admin.postEvent(event);
	}

	// InstructionHandler

	@Override
	public boolean handlesTopic(String topic) {
		return InstructionHandler.TOPIC_SET_CONTROL_PARAMETER.equals(topic);
	}

	@Override
	public InstructionStatus processInstruction(Instruction instruction) {
		BacnetWritePropertyConfig[] configs = getPropConfigs();
		if ( !InstructionHandler.TOPIC_SET_CONTROL_PARAMETER.equals(instruction.getTopic())
				|| configs == null || configs.length < 1 ) {
			return null;
		}
		// look for a parameter name that matches a control ID
		for ( String paramName : instruction.getParameterNames() ) {
			log.trace("Got instruction parameter {}", paramName);
			BacnetWritePropertyConfig config = configForControlId(paramName);
			if ( config == null || !config.isValid() ) {
				continue;
			}
			log.debug("Inspecting instruction {} against control {}", instruction.getId(),
					config.getControlId());
			// treat parameter value as a boolean String
			String str = instruction.getParameterValue(paramName);
			Object desiredValue = controlValueForParameterValue(config, str);
			boolean success = false;
			try {
				success = setValue(config, desiredValue);
			} catch ( Exception e ) {
				log.warn("Error handling instruction {} on control {}: {}", instruction.getTopic(),
						config.getControlId(), e.getMessage());
			}
			if ( success ) {
				postControlEvent(newSimpleNodeControlInfoDatum(config, desiredValue),
						NodeControlProvider.EVENT_TOPIC_CONTROL_INFO_CHANGED);
				return InstructionUtils.createStatus(instruction, InstructionState.Completed);
			}
			return InstructionUtils.createStatus(instruction, InstructionState.Declined);
		}
		return null;
	}

	// SettingSpecifierProvider

	@Override
	public String getSettingUid() {
		return SETTING_UID;
	}

	@Override
	public String getDisplayName() {
		return "BACnet Control";
	}

	private String sampleMessage() {
		BacnetWritePropertyConfig[] configs = getPropConfigs();
		if ( configs == null || configs.length < 1 || propertyValues.isEmpty() ) {
			return "N/A";
		}
		Locale l = Locale.getDefault();
		MessageSource msgSrc = getMessageSource();
		StringBuilder buf = new StringBuilder();
		buf.append(msgSrc.getMessage("sample.markup.start", null, l));
		for ( Entry e : propertyValues.entrySet() ) {
			buf.append(msgSrc.getMessage("sample.markup.row", new Object[] { e.getKey(), e.getValue() },
					l));
		}
		buf.append(msgSrc.getMessage("sample.markup.end", null, l));
		return buf.toString();
	}

	@Override
	public List getSettingSpecifiers() {
		List results = new ArrayList(20);

		// get current value
		results.add(new BasicTitleSettingSpecifier("sample", sampleMessage(), true, true));

		results.add(new BasicTextFieldSettingSpecifier("uid", null));
		results.add(new BasicTextFieldSettingSpecifier("groupUid", null));
		results.add(new BasicTextFieldSettingSpecifier("bacnetNetworkUid", null));

		results.add(new BasicTextFieldSettingSpecifier("sampleCacheMs",
				String.valueOf(DEFAULT_SAMPLE_CACHE_MS)));

		BacnetWritePropertyConfig[] confs = getPropConfigs();
		List confsList = (confs != null ? Arrays.asList(confs)
				: Collections. emptyList());
		results.add(SettingUtils.dynamicListSettingSpecifier("propConfigs", confsList,
				new SettingUtils.KeyedListCallback() {

					@Override
					public Collection mapListSettingKey(
							BacnetWritePropertyConfig value, int index, String key) {
						BasicGroupSettingSpecifier configGroup = new BasicGroupSettingSpecifier(
								BacnetWritePropertyConfig.settings(key + "."));
						return Collections. singletonList(configGroup);
					}
				}));

		return results;
	}

	private SimpleNodeControlInfoDatum currentValue(BacnetWritePropertyConfig config, Object propVal)
			throws IOException {
		if ( propVal == null ) {
			return null;
		}
		return newSimpleNodeControlInfoDatum(config, extractControlValue(config, propVal));
	}

	private Object extractControlValue(BacnetWritePropertyConfig config, Object propVal) {
		if ( propVal instanceof Number ) {
			if ( config.getUnitMultiplier() != null ) {
				propVal = applyUnitMultiplier((Number) propVal, config.getUnitMultiplier());
			}
			if ( config.getDecimalScale() >= 0 ) {
				propVal = applyDecimalScale((Number) propVal, config.getDecimalScale());
			}
		}
		return propVal;
	}

	private Number applyDecimalScale(Number value, int decimalScale) {
		if ( decimalScale < 0 ) {
			return value;
		}
		BigDecimal v = NumberUtils.bigDecimalForNumber(value);
		if ( v.scale() > decimalScale ) {
			v = v.setScale(decimalScale, RoundingMode.HALF_UP);
		}
		return v;
	}

	private Number applyUnitMultiplier(Number value, BigDecimal multiplier) {
		if ( BigDecimal.ONE.compareTo(multiplier) == 0 ) {
			return value;
		}
		BigDecimal v = NumberUtils.bigDecimalForNumber(value);
		return v.multiply(multiplier);
	}

	private Number applyReverseUnitMultiplier(Number value, BigDecimal multiplier) {
		if ( BigDecimal.ONE.compareTo(multiplier) == 0 ) {
			return value;
		}
		BigDecimal v = NumberUtils.bigDecimalForNumber(value);
		return v.divide(multiplier);
	}

	/**
	 * Get the BACNet connection, if available.
	 * 
	 * @return the connection, or {@literal null} it not available
	 */
	protected synchronized BacnetConnection connection() {
		final String networkUid = getBacnetNetworkUid();
		if ( networkUid == null ) {
			return null;
		}
		BacnetNetwork network = service(bacnetNetwork);
		if ( network == null ) {
			return null;
		}
		if ( propertyRefs == null ) {
			Map refs = propertyRefs();
			if ( refs != null ) {
				network.setCachePolicy(refs.keySet(), sampleCacheMs);
			}
		}
		BacnetConnection conn = network.createConnection();
		if ( conn != null ) {
			log.debug("BACnet connection created for {}", networkUid);
		}
		return conn;
	}

	private synchronized Map propertyRefs() {
		if ( propertyRefs != null ) {
			return propertyRefs;
		}
		final BacnetWritePropertyConfig[] propConfs = getPropConfigs();
		if ( propConfs == null || propConfs.length < 1 ) {
			Map results = Collections
					.emptyMap();
			propertyRefs = results;
			return results;
		}
		Map results = new HashMap<>();
		for ( BacnetWritePropertyConfig propConf : propConfs ) {
			if ( !propConf.isValid() ) {
				continue;
			}
			results.put(propConf.toRef(), propConf);
		}
		propertyRefs = results;
		return results;
	}

	/**
	 * Set a BACnet property.
	 * 
	 * @param config
	 *        the configuration of the property to set
	 * @param desiredValue
	 *        the desired value to set, which should have been returned from
	 *        {@link #controlValueForParameterValue(BacnetWritePropertyConfig, String)}
	 * @return {@literal true} if the write succeeded
	 * @throws IOException
	 *         if an IO error occurs
	 */
	private synchronized boolean setValue(final BacnetWritePropertyConfig config,
			final Object desiredValue) throws IOException {
		log.info("Setting {} value to {}", config.getControlId(), desiredValue);
		try (BacnetConnection conn = connection()) {
			if ( conn != null ) {
				conn.updatePropertyValues(Collections.singletonMap(config.toRef(), desiredValue));
				return true;
			}
		}
		return false;
	}

	private Object controlValueForParameterValue(BacnetWritePropertyConfig config, String str) {
		Object result = null;
		switch (config.getControlPropertyType()) {
			case Boolean:
				result = StringUtils.parseBoolean(str);
				break;

			case Float:
			case Percent:
				result = new BigDecimal(str);
				break;

			case Integer:
				result = new BigInteger(str);
				break;

			case String:
				result = str;
				break;

			default:
				// nothing to do

		}

		if ( result != null ) {
			if ( result instanceof Number && config.getUnitMultiplier() != null ) {
				result = applyReverseUnitMultiplier((Number) result, config.getUnitMultiplier());
			}
			return result;
		}

		log.info("Unsupported property type {} for control {}); cannot extract value",
				config.getControlPropertyType(), config.getControlId());
		return null;
	}

	/**
	 * Get the BacnetNetwork service UID filter value.
	 * 
	 * @return the BacnetNetwork UID filter value, if {@code bacnetNetwork} also
	 *         implements {@link FilterableService}
	 */
	public String getBacnetNetworkUid() {
		String uid = FilterableService.filterPropValue(bacnetNetwork, "uid");
		if ( uid != null && uid.trim().isEmpty() ) {
			uid = null;
		}
		return uid;
	}

	/**
	 * Set the BacnetNetwork service UID filter value.
	 * 
	 * @param uid
	 *        the BacnetNetwork UID filter value to set, if
	 *        {@code bacnetNetwork} also implements {@link FilterableService}
	 */
	public void setBacnetNetworkUid(String uid) {
		FilterableService.setFilterProp(bacnetNetwork, "uid", uid);
	}

	/**
	 * Get the sample cache maximum age, in milliseconds.
	 * 
	 * @return the cache milliseconds
	 */
	public long getSampleCacheMs() {
		return sampleCacheMs;
	}

	/**
	 * Set the sample cache maximum age, in milliseconds.
	 * 
	 * @param sampleCacheMs
	 *        the cache milliseconds
	 */
	public void setSampleCacheMs(long sampleCacheMs) {
		this.sampleCacheMs = sampleCacheMs;
	}

	/**
	 * Get the event admin service.
	 * 
	 * @return the event admin
	 */
	public OptionalService getEventAdmin() {
		return eventAdmin;
	}

	/**
	 * Set the event admin sevice.
	 * 
	 * @param eventAdmin
	 *        the service to set
	 */
	public void setEventAdmin(OptionalService eventAdmin) {
		this.eventAdmin = eventAdmin;
	}

	/**
	 * Get the property configurations.
	 * 
	 * @return the property configurations
	 */
	public BacnetWritePropertyConfig[] getPropConfigs() {
		return propConfigs;
	}

	/**
	 * Get the property configurations to use.
	 * 
	 * @param propConfigs
	 *        the configs to use
	 */
	public void setPropConfigs(BacnetWritePropertyConfig[] propConfigs) {
		this.propConfigs = propConfigs;
	}

	/**
	 * Get the number of configured {@code propConfigs} elements.
	 * 
	 * @return the number of {@code propConfigs} elements
	 */
	public int getPropConfigsCount() {
		BacnetWritePropertyConfig[] confs = this.propConfigs;
		return (confs == null ? 0 : confs.length);
	}

	/**
	 * Adjust the number of configured {@code propConfigs} elements.
	 * 
	 * 

* Any newly added element values will be set to new * {@link BacnetWritePropertyConfig} instances. *

* * @param count * The desired number of {@code propConfigs} elements. */ public void setPropConfigsCount(int count) { this.propConfigs = ArrayUtils.arrayWithLength(this.propConfigs, count, BacnetWritePropertyConfig.class, null); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy