/* * Copyright (C) 2015 Alexander Christian <alex(at)root1.de>. All rights reserved. * * This file is part of KnxAutomationDaemon (KAD). * * KAD 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. * * KAD 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 KAD. If not, see <http://www.gnu.org/licenses/>. */ package de.root1.kad.knxservice; import de.root1.kad.KadService; import de.root1.kad.utils.Utils; import de.root1.kad.utils.folderwatch.DefaultFolderWatchListener; import de.root1.kad.utils.folderwatch.FolderWatch; import de.root1.kad.utils.folderwatch.TooMuchFilesException; import de.root1.knxprojparser.GroupAddress; import de.root1.knxprojparser.KnxProjParser; import de.root1.knxprojparser.Project; import de.root1.slicknx.GroupAddressEvent; import de.root1.slicknx.GroupAddressListener; import de.root1.slicknx.Knx; import de.root1.slicknx.KnxException; import de.root1.slicknx.KnxFormatException; import java.io.File; import java.io.FileFilter; 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 java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author achristian */ public class KnxServiceImpl extends KadService implements KnxService { private Logger log = LoggerFactory.getLogger(getClass()); private static final String defaultIA = "1.0.254"; private Knx knx; private final File confFolder = new File(System.getProperty("kad.basedir"), "conf"); /** * User config, that enhances central storage with data unknown to ets * project */ private File knxProject = new File(confFolder, "knxproject.xml"); /** * GA Name -> ListenerList */ private final Map<String, Set<KnxServiceDataListener>> listeners = new HashMap<>(); ExecutorService pool = Executors.newCachedThreadPool(new NamedThreadFactory("KnxServiceOndataForwarding")); private GroupAddressListener gal; private final KnxCache cache; public KnxServiceImpl() { super(); cache = new KnxCache(configProperties); this.gal = new GroupAddressListener() { private void processEvent(GroupAddressEvent event) { KnxServiceDataListener.TYPE type = KnxServiceDataListener.TYPE.WRITE; String ga = null; String dpt = null; String gaName = null; try { ga = event.getDestination(); gaName = translateGaToName(ga); dpt = gaToDpt.get(ga); if (dpt == null) { log.error("No DPT for GA [{}({})] available in config. Skip processing.", ga, gaName); return; } String[] split = dpt.split("\\."); int mainType = Integer.parseInt(split[0]); if (mainType == 0) { log.warn("DPT for GA [{}] is invalid. Check your configuration!", gaName); return; } String value = event.asString(mainType, dpt); // slicknx/calimero string style switch (event.getType()) { case GROUP_READ: type = KnxServiceDataListener.TYPE.READ; value = null; break; case GROUP_RESPONSE: type = KnxServiceDataListener.TYPE.RESPONSE; value = KnxSimplifiedTranslation.decode(dpt, value); break; case GROUP_WRITE: type = KnxServiceDataListener.TYPE.WRITE; break; } fireKnxEvent(ga, gaName, dpt, value, type); } catch (KnxFormatException ex) { log.warn("Error converting data to String with DPT" + dpt + ". event=" + event + " ga=" + ga + " gaName='" + gaName + "'", ex); } catch (KnxServiceConfigurationException ex) { log.warn("Error converting groupaddress to groupaddress-name. ga unknown?. ga=" + ga, ex); } } @Override public void readRequest(GroupAddressEvent event) { processEvent(event); } @Override public void readResponse(GroupAddressEvent event) { processEvent(event); } @Override public void write(final GroupAddressEvent event) { processEvent(event); } }; log.info("Reading knx project data ..."); readKnxProjectData(); try { FolderWatch fw = new FolderWatch("Conf"); fw.addListener(confFolder, new FileFilter() { @Override public boolean accept(File f) { return f.getName().endsWith(".knxproj") || f.getName().equals(knxProject.getName()); } }, new DefaultFolderWatchListener() { @Override public void modified(File f) { log.info("Change in {} detected. Reload scheduled.", f.getName()); readKnxProjectData(); } }); } catch (TooMuchFilesException ex) { log.error("Error observing conf file", ex); } try { // TODO: configure KNX according to properties! knx = new Knx(); knx.setGlobalGroupAddressListener(gal); knx.setIndividualAddress(configProperties.getProperty("knx.individualaddress", defaultIA)); } catch (KnxException ex) { log.error("Error setting up knx access", ex); } } private void fireKnxEvent(final String ga, final String finalGaName, final String dpt, String value, final KnxServiceDataListener.TYPE type) { final String finalValue = KnxSimplifiedTranslation.decode(dpt, value); // convert to KAD string style (no units etc...) switch (type) { case RESPONSE: case WRITE: cache.update(ga, finalValue); break; } synchronized (listeners) { // get listeners for this specific ga name Set<KnxServiceDataListener> list = listeners.get(ga); if (list == null) { log.debug("There's no special listener for [{}@{}]", finalGaName, ga); list = new HashSet<>(); } else { log.debug("{} listeners for [{}@{}]", list.size(), finalGaName, ga); } // get also wildcard listeners, listening for all addresses final Set<KnxServiceDataListener> globalList = listeners.get("*"); if (globalList != null) { log.debug("{} wildcard listeners", globalList.size()); } if (dpt == null || dpt.startsWith("-1")) { log.error("There's no DPT for [" + finalGaName + "@" + ga + "] known?! Can not read --> will not forward. Skipping."); return; } for (final KnxServiceDataListener listener : list) { // execute listener async in thread-pool Runnable r = new Runnable() { @Override public void run() { log.info("Forwarding '{}' with '{}' to [{}@{}]->{}", new Object[]{finalValue, dpt, finalGaName, ga, listener}); listener.onData(finalGaName, finalValue, type); } }; pool.execute(r); } if (globalList != null) { for (final KnxServiceDataListener listener : globalList) { // execute listener async in thread-pool Runnable r = new Runnable() { @Override public void run() { log.debug("Forwarding wildcard '{}' with '{}' to [{}@{}]->{}", new Object[]{finalValue, dpt, finalGaName, ga, listener}); listener.onData(finalGaName, finalValue, type); } }; pool.execute(r); } } } } /** * NAME -> GA */ private Map<String, String> nameToGa = new HashMap<>(); /** * GA -> NAME */ private Map<String, String> gaToName = new HashMap<>(); /** * GA -> DPT */ private Map<String, String> gaToDpt = new HashMap<>(); private final Object DATA_LOCK = new Object(); private void readKnxProjectData() { synchronized (DATA_LOCK) { /** * TODO Install file observer for .knxproj file --> reparse on * change Install file observer for knxproject.xml file --> reparse * on change */ boolean ok = false; try { File[] knxprojFiles = confFolder.listFiles(new FileFilter() { @Override public boolean accept(File f) { return f.getName().endsWith(".knxproj"); } }); Project project = null; if (knxprojFiles != null && knxprojFiles.length == 1) { log.info("Reading {} ... ", knxprojFiles[0].getName()); KnxProjParser parser = new KnxProjParser(); boolean exportXml = parser.exportXml(knxprojFiles[0], knxProject); if (!exportXml) { log.info("No change in knxproj detected. Using last parser result."); } project = parser.getProject(); } else { String msg = ""; if (knxprojFiles != null) { if (knxprojFiles.length == 0) { msg = "No .knxproj file detected in conf folder."; } else if (knxprojFiles.length > 1) { msg = "More than one .knxproj file detected in conf folder. Skip reading knxproj file."; } } else { msg = "No .knxproj file detected in conf folder."; } log.warn(msg); KnxProjParser parser = new KnxProjParser(); parser.readXml(knxProject); project = parser.getProject(); } if (project != null) { log.info("Using KNX project: name=[{}] lastModified=[{}]", project.getName(), project.getLastModified()); List<GroupAddress> groupaddressList = project.getGroupaddressList(); log.info("Found GAs: {}", groupaddressList.size()); for (GroupAddress ga : groupaddressList) { if (KnxProjParser.hasDPT(ga)) { gaToDpt.put(ga.getAddress(), ga.getDPT()); } gaToName.put(ga.getAddress(), ga.getName()); nameToGa.put(ga.getName(), ga.getAddress()); } ok = true; } } catch (Exception ex) { log.warn("Error while reading file data", ex); } finally { if (!ok) { log.warn("GA and DPT cache not available"); nameToGa.clear(); gaToDpt.clear(); } } } } @Override public void writeResponse(String gaName, String value) throws KnxServiceException { String ga = translateNameToGa(gaName); String dpt = getDPT(gaName); try { knx.write(true, ga, dpt, value); fireKnxEvent(ga, gaName, dpt, value, KnxServiceDataListener.TYPE.RESPONSE); } catch (KnxException ex) { throw new KnxServiceException("Problem writing '" + value + "' with DPT " + dpt + " to " + ga, ex); } } @Override public void writeResponse(String individualAddress, String gaName, String value) throws KnxServiceException { try { knx.setIndividualAddress(individualAddress); writeResponse(gaName, value); knx.setIndividualAddress(defaultIA); } catch (KnxServiceException | KnxException ex) { throw new KnxServiceException("Problem writing", ex); } } @Override public void write(String gaName, String value) throws KnxServiceException { String ga = translateNameToGa(gaName); String dpt = getDPT(gaName); try { knx.write(false, ga, dpt, KnxSimplifiedTranslation.encode(dpt, value)); fireKnxEvent(ga, gaName, dpt, value, KnxServiceDataListener.TYPE.WRITE); } catch (KnxException ex) { throw new KnxServiceException("Problem writing '" + value + "' with DPT " + dpt + " to " + ga, ex); } } @Override public void write(String individualAddress, String gaName, String value) throws KnxServiceException { try { knx.setIndividualAddress(individualAddress); write(gaName, value); knx.setIndividualAddress(defaultIA); } catch (KnxServiceException | KnxException ex) { throw new KnxServiceException("Problem writing", ex); } } @Override public String read(String gaName) throws KnxServiceException { String ga = translateNameToGa(gaName); String dpt = getDPT(gaName); String value = cache.get(ga); if (value == null) { try { value = knx.read(ga, dpt); log.info("Cache does not contain value for [{}@{}], reading from bus.", gaName, ga); value = KnxSimplifiedTranslation.decode(dpt, value); cache.update(ga, value); return value; } catch (KnxException ex) { throw new KnxServiceException("Problem reading with DPT " + dpt + " from " + ga, ex); } } else { return value; } } @Override public String read(String individualAddress, String gaName) throws KnxServiceException { try { knx.setIndividualAddress(individualAddress); String read = read(gaName); knx.setIndividualAddress(defaultIA); return read; } catch (KnxServiceException | KnxException ex) { throw new KnxServiceException("Problem writing", ex); } } @Override public void registerListener(String gaName, KnxServiceDataListener listener) throws KnxServiceConfigurationException { if (gaName == null || gaName.isEmpty()) { throw new IllegalArgumentException("gaName must not be null or empty"); } if (listener == null) { throw new IllegalArgumentException("lister must not be null"); } String ga = translateNameToGa(gaName); log.info("[{}->{}] --> {}", gaName, ga, listener); synchronized (listeners) { Set<KnxServiceDataListener> list = listeners.get(ga); if (list == null) { list = new HashSet<>(); listeners.put(ga, list); } boolean isNew = list.add(listener); if (!isNew) { log.warn("Tried to add the listener {} multiple times to {}", listener, gaName); } } } @Override public void unregisterListener(String gaName, KnxServiceDataListener listener) throws KnxServiceConfigurationException { if (gaName == null || gaName.isEmpty()) { throw new IllegalArgumentException("gaName must not be null or empty"); } if (listener == null) { throw new IllegalArgumentException("lister must not be null"); } String ga = translateNameToGa(gaName); log.debug("[{}->{}]", gaName, ga); synchronized (listeners) { Set<KnxServiceDataListener> list = listeners.get(ga); if (list != null) { list.remove(listener); if (list.isEmpty()) { listeners.remove(ga); } } } } @Override protected Class getServiceClass() { return KnxService.class; } @Override public String translateGaToName(String ga) throws KnxServiceConfigurationException { synchronized (DATA_LOCK) { if (gaToName.containsKey(ga)) { return gaToName.get(ga); } } log.debug("translation not possible: {}", ga); return ""; } @Override public String translateNameToGa(String gaName) throws KnxServiceConfigurationException { synchronized (DATA_LOCK) { if (nameToGa.containsKey(gaName)) { return nameToGa.get(gaName); } } if (Utils.isName(gaName)) { throw new KnxServiceConfigurationException("Group address [" + gaName + "] ist not configured and cannot be translated to a valid GA. Check your config!."); } log.debug("translation not possible: {}", gaName); return gaName; } @Override public String getDPT(String gaName) throws KnxServiceConfigurationException { String ga; String dpt; synchronized (DATA_LOCK) { ga = translateNameToGa(gaName); dpt = gaToDpt.get(ga); } if (dpt == null) { throw new KnxServiceConfigurationException("Group address [" + gaName + "@" + ga + "] has no associated DPT. Please update configuration."); } return dpt; } @Override public String getCachedValue(String gaName) throws KnxServiceException { String value = null; try { String ga = translateNameToGa(gaName); value = cache.get(ga); } catch (KnxServiceException ex) { throw new KnxServiceException("Problem translating '" + gaName + "' to GA", ex); } return value; } }