/*
* 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;
}
}