/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.ebus.internal.parser; import java.math.BigDecimal; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.script.Bindings; import javax.script.CompiledScript; import javax.script.ScriptException; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.openhab.binding.ebus.internal.EBusTelegram; import org.openhab.binding.ebus.internal.configuration.TelegramConfiguration; import org.openhab.binding.ebus.internal.configuration.TelegramValue; import org.openhab.binding.ebus.internal.utils.EBusCodecUtils; import org.openhab.binding.ebus.internal.utils.EBusUtils; import org.openhab.binding.ebus.internal.utils.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class parses received eBus telegrams from eBus and convert them to * a result map with keys and resolved values. * * @author Christian Sowada * @since 1.7.0 */ public class EBusTelegramParser { private static final Logger logger = LoggerFactory.getLogger(EBusTelegramParser.class); private static final Logger loggerAnalyses = LoggerFactory .getLogger(EBusTelegramParser.class.getPackage().getName() + ".Analyses"); private static final Logger loggerBrutforce = LoggerFactory .getLogger(EBusTelegramParser.class.getPackage().getName() + ".BruteForce"); // The configuration provider to parse the ebus telegram private EBusConfigurationProvider configurationProvider; private EBusTelegramCSVWriter debugWriter; private String debugWriteMode; /** * Constructor * * @param configurationProvider */ public EBusTelegramParser(EBusConfigurationProvider configurationProvider) { this.configurationProvider = configurationProvider; } /** * Sets the csv writer to log telegrams * * @param debugWriter * @param debugWriteMode */ public void setDebugCSVWriter(EBusTelegramCSVWriter debugWriter, String debugWriteMode) { this.debugWriter = debugWriter; this.debugWriteMode = debugWriteMode; } /** * @param byteBuffer * @param telegramValue * @param type * @param pos * @return */ private Object getValue(ByteBuffer byteBuffer, TelegramValue telegramValue) { String type = telegramValue.getType().toLowerCase(); int pos = telegramValue.getPos() != null ? telegramValue.getPos() : -1; Object value = null; // requested pos is greater as whole buffer if (pos > byteBuffer.position()) { logger.warn("eBus buffer pos error! Can happen ..."); } // replace similar data types if (type.equals("uint")) { type = "word"; } if (type.equals("byte")) { type = "uchar"; } byte[] bytes = null; if (type.equals("data2b") || type.equals("data2c") || type.equals("word")) { bytes = new byte[] { byteBuffer.get(pos), byteBuffer.get(pos - 1) }; } else { bytes = new byte[] { byteBuffer.get(pos - 1) }; } if (type.equals("bit")) { int bit = telegramValue.getBit(); value = bytes[0]; boolean isSet = ((Byte) value >> bit & 0x1) == 1; value = isSet; } else { value = NumberUtils.toBigDecimal(EBusCodecUtils.decode(type, bytes, telegramValue.getReplaceValue())); } // if BigDecimal check for min, max and replace value if (value instanceof BigDecimal) { BigDecimal b = (BigDecimal) value; // multiply before check min and max if (b != null && telegramValue.getFactor() != null) { logger.trace("Value multiplied ..."); value = b = b.multiply(telegramValue.getFactor()); } // value is below min value, return null if (telegramValue.getMin() != null && b != null && b.compareTo(telegramValue.getMin()) == -1) { logger.trace("Minimal value reached, skip value ..."); value = b = null; // value is above max value, return null } else if (telegramValue.getMax() != null && b != null && b.compareTo(telegramValue.getMax()) == 1) { logger.trace("Maximal value reached, skip value ..."); value = b = null; } } return value; } /** * Evaluates the compiled script of a entry. * * @param entry The configuration entry to evaluate * @param scopeValues All known values for script scope * @return The computed value * @throws ScriptException */ private Object evaluateScript(Entry<String, TelegramValue> entry, Map<String, Object> scopeValues) throws ScriptException { Object value = null; // executes compiled script if (entry.getValue().getCsript() != null) { CompiledScript cscript = entry.getValue().getCsript(); // Add global variables thisValue and keyName to JavaScript context Bindings bindings = cscript.getEngine().createBindings(); bindings.putAll(scopeValues); value = cscript.eval(bindings); } // try to convert the returned value to BigDecimal value = ObjectUtils.defaultIfNull(NumberUtils.toBigDecimal(value), value); // round to two digits, maybe not optimal for any result if (value instanceof BigDecimal) { ((BigDecimal) value).setScale(2, BigDecimal.ROUND_HALF_UP); } return value; } /** * A function show data from unknown telegrams * * @param telegram */ private void bruteforceEBusTelegram(EBusTelegram telegram) { byte[] data = telegram.getData(); String format = String.format("%-4s%-13s%-13s%-13s%-13s%-13s%-13s", "Pos", "WORD", "UInt", "DATA2B", "DATA2C", "DATA1c", "BCD"); loggerBrutforce.trace(" " + format); loggerBrutforce.trace(" -----------------------------------------------------------------------------"); // Check all possible positions with known data types for (int i = 0; i < data.length; i++) { Object word = i == data.length - 1 ? "---" : EBusCodecUtils.decodeWord(new byte[] { data[i + 1], data[i] }); Object data2b = i == data.length - 1 ? "---" : EBusCodecUtils.decodeDATA2b(new byte[] { data[i + 1], data[i] }); Object data2c = i == data.length - 1 ? "---" : EBusCodecUtils.decodeDATA2c(new byte[] { data[i + 1], data[i] }); Object data1c = i == data.length - 1 ? "---" : EBusCodecUtils.decodeDATA1c(data[i + 1]); int bcd = EBusCodecUtils.decodeBCD(data[i]); int uint = data[i] & 0xFF; format = String.format("%-4s%-13s%-13s%-13s%-13s%-13s%-13s", i + 6, word, uint, data2b, data2c, data1c, bcd); loggerBrutforce.trace(" " + format); } // Parse slave data if (telegram.getType() == EBusTelegram.TYPE_MASTER_SLAVE) { data = telegram.getSlaveData(); loggerBrutforce.trace(" ---------------------------------- Answer ----------------------------------"); for (int i = 0; i < data.length; i++) { Object word = i == data.length - 1 ? "---" : EBusCodecUtils.decodeWord(new byte[] { data[i + 1], data[i] }); Object data2b = i == data.length - 1 ? "---" : EBusCodecUtils.decodeDATA2b(new byte[] { data[i + 1], data[i] }); Object data2c = i == data.length - 1 ? "---" : EBusCodecUtils.decodeDATA2c(new byte[] { data[i + 1], data[i] }); Object data1c = i == data.length - 1 ? "---" : EBusCodecUtils.decodeDATA1c(data[i + 1]); int bcd = EBusCodecUtils.decodeBCD(data[i]); int uint = data[i] & 0xFF; format = String.format("%-4s%-13s%-13s%-13s%-13s%-13s%-13s", i + 6, word, uint, data2b, data2c, data1c, bcd); loggerBrutforce.trace(" " + format); } } } /** * Parses a valid eBus telegram and returns a map with key/values based on * configuration registry. * * @param telegram The eBus telegram * @return A Map with parsed key/values */ public Map<String, Object> parse(EBusTelegram telegram) { // Check if a configuration provider is set if (configurationProvider == null) { logger.error("Configuration not loaded, can't parse telegram!"); return null; } // Secure null check if (telegram == null) { return null; } // All parsed values final Map<String, Object> valueRegistry = new HashMap<String, Object>(); // All parsed values with short keys, used for script evaluation final Map<String, Object> valueRegistryShortKeys = new HashMap<String, Object>(); // Get as byte buffer final ByteBuffer byteBuffer = telegram.getBuffer(); // Get hex string for debugging final String bufferString = EBusUtils.toHexDumpString(byteBuffer).toString(); // queries the configuration provider for matching registry entries final List<TelegramConfiguration> matchedTelegramRegistry = configurationProvider .getCommandsByFilter(bufferString); loggerAnalyses.debug(bufferString); // No registry entries found, so this is a unknown telegram if (matchedTelegramRegistry.isEmpty()) { if (debugWriter != null && (debugWriteMode.contains("unknown") || debugWriteMode.contains("all"))) { debugWriter.writeTelegram(telegram, "<unknown>"); } loggerAnalyses.debug(" >>> Unknown ----------------------------------------"); if (loggerBrutforce.isTraceEnabled()) { loggerBrutforce.trace(bufferString); bruteforceEBusTelegram(telegram); } return null; } // loop thru all matching telegrams from registry for (TelegramConfiguration registryEntry : matchedTelegramRegistry) { int debugLevel = 0; // get id and class key if used String idKey = StringUtils.defaultString(registryEntry.getId()); String classKey = StringUtils.defaultString(registryEntry.getClazz()); // load debug level for this configuration entry if available if (registryEntry.getDebug() != null) { debugLevel = registryEntry.getDebug(); if (debugWriter != null && debugWriteMode.contains("debug")) { debugWriter.writeTelegram(telegram, "DEBUG:" + registryEntry.getComment()); } } if (debugWriter != null && debugWriteMode.equals("all")) { debugWriter.writeTelegram(telegram, registryEntry.getComment()); } // get values block of configuration Map<String, TelegramValue> values = registryEntry.getValues(); // debug loggerAnalyses.debug(" >>> {}", StringUtils.defaultIfEmpty(registryEntry.getComment(), "<No comment available>")); TelegramValue settings = null; // loop over all entries for (Entry<String, TelegramValue> entry : values.entrySet()) { String uniqueKey = (classKey != "" ? classKey + "." : "") + (idKey != "" ? idKey + "." : "") + entry.getKey(); settings = entry.getValue(); // Extract the value from byte buffer Object value = getValue(byteBuffer, entry.getValue()); if(value == null) { // its okay if the value is null, maybe out of range min/max or replace value found logger.trace("Returned value is null, skip ..."); continue; } // If compiled script available for this key, execute it now if (settings.getCsript() != null) { try { // Add global variables thisValue and keyName to JavaScript context HashMap<String, Object> bindings = new HashMap<String, Object>(); bindings.put(entry.getKey(), value); // short key bindings.put(uniqueKey, value); // full key bindings.put("thisValue", value); // alias thisValue // Evaluates script value = evaluateScript(entry, bindings); } catch (ScriptException e) { logger.error("Error on evaluating JavaScript!", e); break; } } // debug String label = StringUtils.defaultString(settings.getLabel()); String format = String.format("%-35s%-10s%s", uniqueKey, value, label); String alias = null; if (settings.getMapping() != null) { Map<String, String> mapping = settings.getMapping(); alias = mapping.get(value.toString()); } if (debugLevel >= 2) { loggerAnalyses.debug(" >>> " + format); if (alias != null) { loggerAnalyses.debug(" >>> " + alias); } } else { loggerAnalyses.trace(" >>> " + format); if (alias != null) { loggerAnalyses.trace(" >>> " + alias); } } // Add result to registry valueRegistry.put(uniqueKey, value); // Add result to temp. short key registry, used for scripts valueRegistryShortKeys.put(entry.getKey(), value); // also use class.id as key as shortcut if we have only one value if (values.size() == 1) { if (!StringUtils.isEmpty(classKey) && !StringUtils.isEmpty(idKey)) { uniqueKey = classKey + "." + idKey; valueRegistry.put(uniqueKey, value); } } } // computes values available? if not exit here if (registryEntry.getComputedValues() == null) { continue; } // post execute the computes_values block Map<String, TelegramValue> cvalues = registryEntry.getComputedValues(); for (Entry<String, TelegramValue> entry : cvalues.entrySet()) { String uniqueKey = (classKey != "" ? classKey + "." : "") + (idKey != "" ? idKey + "." : "") + entry.getKey(); // Add all values to script scope HashMap<String, Object> bindings = new HashMap<String, Object>(); bindings.putAll(valueRegistryShortKeys); bindings.putAll(valueRegistry); Object value; try { // Evaluates script value = evaluateScript(entry, bindings); // Add result to registry valueRegistry.put(uniqueKey, value); if (debugLevel >= 2) { String label = StringUtils.defaultString(settings.getLabel()); String format = String.format("%-35s%-10s%s", uniqueKey, value, label); loggerAnalyses.debug(" >>> " + format); } } catch (ScriptException e) { logger.error("Error on evaluating JavaScript!", e); } } } return valueRegistry; } }