Compare commits

..

4 Commits

Author SHA1 Message Date
holz holz@unicon-gmbh.de 124330db9a Merge kebaAgent/master 2026-08-13 09:19:14 +02:00
holz holz@unicon-gmbh.de 9bf9b9a66f Optimierung der Abfragezeiten, Teilcaching
zusätzlich Timestamp pro alias
2026-08-13 09:13:16 +02:00
holz c560160dd0 README.md hinzugefügt 2026-08-13 05:14:22 +00:00
holz holz@unicon-gmbh.de 726936dada 2 2026-08-13 07:10:05 +02:00
16 changed files with 3049 additions and 15 deletions
-10
View File
@@ -1,10 +0,0 @@
# ---> NetBeans
**/nbproject/private/
**/nbproject/Makefile-*.mk
**/nbproject/Package-*.bash
build/
nbbuild/
dist/
nbdist/
.nb-gradle/
-5
View File
@@ -1,5 +0,0 @@
Copyright (C) 2026 by UniCon
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+73
View File
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- You may freely edit this file. See commented blocks below for -->
<!-- some examples of how to customize the build. -->
<!-- (If you delete it and reopen the project it will be recreated.) -->
<!-- By default, only the Clean and Build commands use this build script. -->
<!-- Commands such as Run, Debug, and Test only use this build script if -->
<!-- the Compile on Save feature is turned off for the project. -->
<!-- You can turn off the Compile on Save (or Deploy on Save) setting -->
<!-- in the project's Project Properties dialog box.-->
<project name="kebaAgent" default="default" basedir=".">
<description>Builds, tests, and runs the project kebaAgent.</description>
<import file="nbproject/build-impl.xml"/>
<!--
There exist several targets which are by default empty and which can be
used for execution of your tasks. These targets are usually executed
before and after some main targets. They are:
-pre-init: called before initialization of project properties
-post-init: called after initialization of project properties
-pre-compile: called before javac compilation
-post-compile: called after javac compilation
-pre-compile-single: called before javac compilation of single file
-post-compile-single: called after javac compilation of single file
-pre-compile-test: called before javac compilation of JUnit tests
-post-compile-test: called after javac compilation of JUnit tests
-pre-compile-test-single: called before javac compilation of single JUnit test
-post-compile-test-single: called after javac compilation of single JUunit test
-pre-jar: called before JAR building
-post-jar: called after JAR building
-post-clean: called after cleaning build products
(Targets beginning with '-' are not intended to be called on their own.)
Example of inserting an obfuscator after compilation could look like this:
<target name="-post-compile">
<obfuscate>
<fileset dir="${build.classes.dir}"/>
</obfuscate>
</target>
For list of available properties check the imported
nbproject/build-impl.xml file.
Another way to customize the build is by overriding existing main targets.
The targets of interest are:
-init-macrodef-javac: defines macro for javac compilation
-init-macrodef-junit: defines macro for junit execution
-init-macrodef-debug: defines macro for class debugging
-init-macrodef-java: defines macro for class execution
-do-jar: JAR building
run: execution of project
-javadoc-build: Javadoc generation
test-report: JUnit report generation
An example of overriding the target for project execution could look like this:
<target name="run" depends="kebaAgent-impl.jar">
<exec dir="bin" executable="launcher.exe">
<arg file="${dist.jar}"/>
</exec>
</target>
Notice that the overridden target depends on the jar target and not only on
the compile target as the regular run target does. Again, for a list of available
properties which you can use, check the target you are overriding in the
nbproject/build-impl.xml file.
-->
</project>
+17
View File
@@ -0,0 +1,17 @@
# Einstellungen UniCon KemaAgent
aliases: links, rechts
links.port: 502
#links.ip: 192.168.0.28
links.ip: localhost
rechts.port: 503
#rechts.ip: 192.168.0.29
rechts.ip: localhost
webserver.port: 50000
webserver.defaultUri: /keba
Binary file not shown.
BIN
View File
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
Manifest-Version: 1.0
X-COMMENT: Main-Class will be added automatically by build
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
build.xml.data.CRC32=11b719f8
build.xml.script.CRC32=606a34af
build.xml.stylesheet.CRC32=f85dc8f2@1.119.0.48
# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
nbproject/build-impl.xml.data.CRC32=11b719f8
nbproject/build-impl.xml.script.CRC32=b9fa6210
nbproject/build-impl.xml.stylesheet.CRC32=12e0a6c2@1.119.0.48
+102
View File
@@ -0,0 +1,102 @@
annotation.processing.enabled=true
annotation.processing.enabled.in.editor=false
annotation.processing.processors.list=
annotation.processing.run.all.processors=true
annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
application.title=kebaAgent
application.vendor=holz
build.classes.dir=${build.dir}/classes
build.classes.excludes=**/*.java,**/*.form
# This directory is removed when the project is cleaned:
build.dir=build
build.generated.dir=${build.dir}/generated
build.generated.sources.dir=${build.dir}/generated-sources
# Only compile against the classpath explicitly listed here:
build.sysclasspath=ignore
build.test.classes.dir=${build.dir}/test/classes
build.test.results.dir=${build.dir}/test/results
# Uncomment to specify the preferred debugger connection transport:
#debug.transport=dt_socket
debug.classpath=\
${run.classpath}
debug.modulepath=\
${run.modulepath}
debug.test.classpath=\
${run.test.classpath}
debug.test.modulepath=\
${run.test.modulepath}
# Files in build.classes.dir which should be excluded from distribution jar
dist.archive.excludes=
# This directory is removed when the project is cleaned:
dist.dir=dist
dist.jar=${dist.dir}/kebaAgent.jar
dist.javadoc.dir=${dist.dir}/javadoc
dist.jlink.dir=${dist.dir}/jlink
dist.jlink.output=${dist.jlink.dir}/kebaAgent
endorsed.classpath=
excludes=
file.reference.jlibmodbus-1.2.9.4.jar=lib\\jlibmodbus-1.2.9.4.jar
file.reference.json.jar=lib\\json.jar
includes=**
jar.compress=false
javac.classpath=\
${file.reference.jlibmodbus-1.2.9.4.jar}:\
${file.reference.json.jar}
# Space-separated list of extra javac options
javac.compilerargs=
javac.deprecation=false
javac.external.vm=true
javac.modulepath=
javac.processormodulepath=
javac.processorpath=\
${javac.classpath}
javac.source=25
javac.target=25
javac.test.classpath=\
${javac.classpath}:\
${build.classes.dir}
javac.test.modulepath=\
${javac.modulepath}
javac.test.processorpath=\
${javac.test.classpath}
javadoc.additionalparam=
javadoc.author=false
javadoc.encoding=${source.encoding}
javadoc.html5=false
javadoc.noindex=false
javadoc.nonavbar=false
javadoc.notree=false
javadoc.private=false
javadoc.splitindex=true
javadoc.use=true
javadoc.version=false
javadoc.windowtitle=
# The jlink additional root modules to resolve
jlink.additionalmodules=
# The jlink additional command line parameters
jlink.additionalparam=
jlink.launcher=true
jlink.launcher.name=kebaAgent
main.class=kebaagent.KebaAgent
manifest.file=manifest.mf
meta.inf.dir=${src.dir}/META-INF
mkdist.disabled=false
no.dependencies=true
platform.active=default_platform
run.classpath=\
${javac.classpath}:\
${build.classes.dir}
# Space-separated list of JVM arguments used when running the project.
# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
# To set system properties for unit tests define test-sys-prop.name=value:
run.jvmargs=
run.modulepath=\
${javac.modulepath}
run.test.classpath=\
${javac.test.classpath}:\
${build.test.classes.dir}
run.test.modulepath=\
${javac.test.modulepath}
source.encoding=UTF-8
src.dir=src
test.src.dir=test
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://www.netbeans.org/ns/project/1">
<type>org.netbeans.modules.java.j2seproject</type>
<configuration>
<data xmlns="http://www.netbeans.org/ns/j2se-project/3">
<name>kebaAgent</name>
<source-roots>
<root id="src.dir"/>
</source-roots>
<test-roots>
<root id="test.src.dir"/>
</test-roots>
</data>
</configuration>
</project>
+100
View File
@@ -0,0 +1,100 @@
/*
*/
package kebaagent;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
* @author holz
*/
class JsonHandler implements HttpHandler
{
private final KebaAgent agent;
JsonHandler(KebaAgent agent)
{
this.agent = agent;
}
@Override
public void handle(HttpExchange httpExchange) throws IOException
{
// nur GET
if (!"GET".equals(httpExchange.getRequestMethod()))
{
return;
}
// Parameter ermitteln: path ist Teil hinter Domain: "/t/allowed?..."
String uri = httpExchange.getRequestURI().toString();
Map<String, String> params = getParams(uri);
String callData = agent.getDataAsJson(params);
// response
OutputStream outputStream = httpExchange.getResponseBody();
// StringBuilder sb = new StringBuilder();
// sb
// .append("<html>")
// .append("<body>")
// .append("<h1>")
// .append("Hello ")
// .append(callData)
// .append("</h1>")
// .append("</body>")
// .append("</html>");
try
{
// String htmlResponse = sb.toString();
String htmlResponse = callData;
// jaho: Header erweitern
Headers responseHeaders = httpExchange.getResponseHeaders();
responseHeaders.set("Content-Type", "application/json");
// this line is a must (jaho: header senden)
httpExchange.sendResponseHeaders(200, htmlResponse.length());
outputStream.write(htmlResponse.getBytes());
outputStream.flush();
outputStream.close();
}
catch (Throwable e)
{
e.printStackTrace();
}
}
private Map<String, String> getParams(String uri)
{
// split bei ?
List<String> l = new ArrayList<>(List.of(uri.split("\\?")));
final Map<String, String> paramsMap = new HashMap();
l.removeFirst();
l.forEach(xparam ->
{
// key/value extrahieren
String[] split = xparam.split("=");
String k = split[0];
String v = split.length > 1 ? split[1] : "";
paramsMap.put(k, v);
});
System.out.println("");
return paramsMap;
}
}
+327
View File
@@ -0,0 +1,327 @@
/*
Webservice bereitstellen und auf http:GET-Anfrage Werte von KEBA-Wallbox per MODBUS holen
*/
package kebaagent;
import com.intelligt.modbus.jlibmodbus.exception.ModbusIOException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusNumberException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusProtocolException;
import com.intelligt.modbus.jlibmodbus.master.ModbusMaster;
import com.intelligt.modbus.jlibmodbus.tcp.TcpParameters;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import static kebaagent.KebaModbus.connect;
import static kebaagent.KebaModbus.readInt32BigEndian;
import static kebaagent.KebaModbus.sleep;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;
/**
* In der Konfigurationsdatei können mehrere Wallbox-Ports aka alias definiert werden. Über die wird die Konfig separiert.
* @author holz
*/
public class KebaAgent
{
public static final int WEBSERVER_PORT = 50000;
private Properties prop = new Properties();
/**
* Liste von Instanzen über Namen
*/
private final Map<String, KebaModbus> kebaModbusMap = new HashMap<>();
/**
* Instanz erstellen und properties einlesen
* @see #init()
* @param args
*/
public KebaAgent(String[] args)
{
String propertiesFilename = "kemaagent.properties";
if (args.length > 0)
{
List<String> lArgs = List.of(args);
if (!lArgs.getLast().startsWith("-"))
{
propertiesFilename = lArgs.getLast();
}
}
System.err.printf("benutzer Einstellungen aus %s\n", propertiesFilename);
try
{
prop.load(new FileReader(propertiesFilename));
}
catch (FileNotFoundException ex)
{
System.getLogger(KebaAgent.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
}
catch (IOException ex)
{
System.getLogger(KebaAgent.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
}
}
/**
*
* @param alias
* @param name
* @return
*/
public Optional<String> getPropValue(String alias, String name)
{
return Optional.of(prop.getProperty(alias + "." + name));
}
/**
* Webserver Port aus config ermitteln und liefern
* @return
*/
int getPort()
{
int defPort = WEBSERVER_PORT;
String key = "webserver.port";
try
{
defPort = Integer.parseInt(prop.getProperty(key, ""));
}
catch (NumberFormatException e)
{
System.err.printf("Schlüssel %s konnte nicht aus properties eingelesen werden. Es wird Standard %d benutzt.\n", key, defPort);
}
return defPort;
}
/**
* alle wichtigen modbus-Daten beschaffen
* @param params
* @return JSON-Daten
*/
String getDataAsJson(Map<String, String> params)
{
final int slaveId = 255;
JSONObject joRoot = new JSONObject();
JSONArray jaStations = new JSONArray();
// über alle Wallboxen ...
getAliases()
.forEach(name ->
{
KebaModbus kebaModbus = kebaModbusMap.get(name);
JSONObject joPart = new JSONObject();
int intVal;
try
{
if (kebaModbus.initFailed)
{
joPart.put("error", "initError");
jaStations.put(joPart);
return;
}
joPart.put("alias", name);
int pause_ms = 600;
// nach keba-Doku muss zwischen den Abfragen 500ms Pause sein und es kann immer nur ein Register gelesen werden!
// sleep(600);
String
jName = "chargingState";
int register = 1000;
intVal = kebaModbus.read(jName, register, slaveId, pause_ms);
// val = readInt32BigEndian(kebaModbus.getModbusMaster(), slaveId, register);
System.out.printf("Wert für reg %d: %d (0x%04x)\n", register, intVal, intVal);
joPart.put(jName, intVal);
// cache-Zeit für statische Daten (S/N etc)
int maxAgeMinutes = 60;
// sleep(600);
jName = "s/n";
register = 1014;
intVal = kebaModbus.readCached(jName, register, slaveId, pause_ms, maxAgeMinutes);
System.out.printf("Wert für reg %d: %d (0x%04x)\n", register, intVal, intVal);
joPart.put(jName, intVal);
// akt. Leistung
// sleep(600);
jName = "power";
register = 1020;
intVal = kebaModbus.read(jName, register, slaveId, pause_ms);
// val = readInt32BigEndian(kebaModbus.getModbusMaster(), slaveId, register);
System.out.printf("Wert für reg %d (active Power): %1.1f kW (0x%04x)\n", register, intVal/1e6, intVal);
joPart.put(jName, intVal/1e6);
// sleep(600);
jName = "RFID";
register = 1500;
intVal = kebaModbus.read(jName, register, slaveId, pause_ms);
// val = readInt32BigEndian(kebaModbus.getModbusMaster(), slaveId, register);
System.out.printf("Wert für reg %d (RFID-Card): %d (0x%04x)\n", register, intVal, intVal);
joPart.put(jName, intVal);
// sleep(600);
jName = "chargedEnergy";
// angeblich Wh, aber in Wirklichkeit Wh/10
register = 1502;
intVal = kebaModbus.read(jName, register, slaveId, pause_ms);
// val = readInt32BigEndian(kebaModbus.getModbusMaster(), slaveId, register);
System.out.printf("Wert für reg %d (charged energy [kWh]): %1.1f (0x%04x)\n", register, intVal/10000f, intVal);
joPart.put(jName, intVal/10000f);
joPart.put("ts", LocalDateTime.now().withNano(0).toString());
jaStations.put(joPart);
}
catch (ModbusProtocolException | ModbusNumberException | ModbusIOException ex)
{
System.getLogger(KebaAgent.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
// joPart.put("Error", "exception: %s".formatted(ex.getMessage());
// return "{ error: 'exception: %s'}".formatted(ex.getMessage());
}
catch (JSONException ex)
{
System.getLogger(KebaAgent.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
}
});
try
{
joRoot.put("stations", jaStations);
return joRoot.toString();
}
catch (JSONException ex)
{
System.getLogger(KebaAgent.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
return "{ error: '%s'".formatted(ex.getMessage());
}
}
/**
*
*/
private void init()
{
getAliases().forEach(alias ->
{
getPropValue(alias, "ip").ifPresent(sIP ->
{
KebaModbus km = new KebaModbus(sIP, getModbusPort(alias));
km.initFailed = true;
try
{
km.init();
km.initFailed = false;
kebaModbusMap.put(alias, km);
}
catch (UnknownHostException | ModbusIOException | NumberFormatException ex)
{
System.getLogger(KebaAgent.class.getName()).log(System.Logger.Level.ERROR, "init-Fehler", ex);
}
});
});
// KebaModbus km = new KebaModbus("localhost", 502);
// kebaModbusMap.put("links", km);
// km.initFailed = true;
// km.init();
// km.initFailed = false;
//
// km = new KebaModbus("localhost", 503);
// kebaModbusMap.put("rechts", km);
// km.initFailed = true;
// km.init();
// km.initFailed = false;
}
/**
*
* @return Liste von aliases aus den properties
*/
private List<String> getAliases()
{
String a = prop.getProperty("aliases", "");
return List.of(a.split(",")).stream().map(s -> s.trim()).toList();
}
/**
* Port aus konfig oder 502
* @param alias
* @return
* @throws NumberFormatException
*/
private int getModbusPort(String alias) throws NumberFormatException
{
Optional<String> oPort = getPropValue(alias, "port");
if (oPort.isPresent())
{
return Integer.parseInt(oPort.get());
}
else
return 502;
}
/**
* @param args the command line arguments
*/
public static void main(String[] args)
{
KebaAgent agent = new KebaAgent(args);
agent.init();
Webserver webserver = new Webserver(agent.getPort());
try
{
webserver.start(agent);
}
catch (IOException ex)
{
System.getLogger(KebaAgent.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
}
System.out.println("webserver gestartet");
}
}
+404
View File
@@ -0,0 +1,404 @@
/*
*/
package kebaagent;
import com.intelligt.modbus.jlibmodbus.Modbus;
import com.intelligt.modbus.jlibmodbus.exception.ModbusIOException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusNumberException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusProtocolException;
import com.intelligt.modbus.jlibmodbus.master.ModbusMaster;
import com.intelligt.modbus.jlibmodbus.master.ModbusMasterFactory;
import com.intelligt.modbus.jlibmodbus.msg.request.ReadHoldingRegistersRequest;
import com.intelligt.modbus.jlibmodbus.msg.response.ReadHoldingRegistersResponse;
import com.intelligt.modbus.jlibmodbus.tcp.TcpParameters;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
//import oracle.jrockit.jfr.tools.ConCatRepository;
/**
* Abfrage KEBA-Wallbox
* @author jaho
*/
public class KebaModbus
{
private int port;
private String ip;
public boolean initFailed;
private ModbusMaster modbusMaster;
private final Map<String, Integer> valueMap = new HashMap();
private static int timeout_ms = 10000;
record RCacheEntry(String name, LocalDateTime ts, Integer value){};
private Map<String, RCacheEntry> cache = new HashMap<>();
/**
*
* @throws UnknownHostException
* @throws ModbusIOException
*/
void init() throws UnknownHostException, ModbusIOException
{
TcpParameters tcpParameters = new TcpParameters();
// TcpParameters tcpParametersSlave = new TcpParameters();
//tcp parameters have already set by default as in example
// tcpParameters.setHost(InetAddress.getLocalHost());
tcpParameters.setHost(InetAddress.getByName(ip));
tcpParameters.setKeepAlive(true);
tcpParameters.setPort(port);
// tcpParametersSlave.setPort(Modbus.TCP_PORT);
/*
// if you would like to set connection parameters separately,
// you should use another method: createModbusMasterTCP(String host, int port, boolean keepAlive);
m = ModbusMasterFactory.createModbusMasterTCP(tcpParameters);
Modbus.setAutoIncrementTransactionId(true);
// jaho
m.setResponseTimeout(60000);
*/
System.err.printf("try to connect to %s:%d ...", ip, port);
// ! diese Methode kommt immer sofort zurück. Man muss Ergebnis prüfen!
modbusMaster = connect(tcpParameters, timeout_ms);
if (modbusMaster.isConnected())
System.err.printf("done\n");
else
{
modbusMaster.connect();
if (!modbusMaster.isConnected())
{
System.err.printf("FAILED\n");
System.exit(1);
}
}
}
/**
*
* @return
*/
public ModbusMaster getModbusMaster()
{
return modbusMaster;
}
/**
*
* @return
*/
public Map<String, Integer> getValueMap()
{
return valueMap;
}
/**
* Keba-Wallbox über MODBUS verbinden
* @param ip
* @param port
*/
public KebaModbus(String ip, int port)
{
this.ip = ip;
this.port = port;
}
/**
* Verbindung herstellen
* @param tcpParameters
* @param responeTimeout_ms
* @return
*/
public static ModbusMaster connect(TcpParameters tcpParameters, int responeTimeout_ms)
{
// if you would like to set connection parameters separately,
// you should use another method: createModbusMasterTCP(String host, int port, boolean keepAlive);
ModbusMaster m = ModbusMasterFactory.createModbusMasterTCP(tcpParameters);
Modbus.setAutoIncrementTransactionId(true);
if (responeTimeout_ms > 0)
m.setResponseTimeout(responeTimeout_ms);
return m;
}
private static void usage()
{
System.err.println("Reading MODBUS-Register from IPAdr\n\nusage:\nSwgModbus <IPAdr> <AdrOffset> <count> [port]\n");
}
/**
* 4 Bytes lesen. Das geht hier nur mit dem Abruf von 2 16-Bit-Registern
* @param slaveId
* @param register Startregister
* @return
*/
static int readInt32BigEndian(ModbusMaster modbusMaster, int slaveId, int register) throws ModbusProtocolException, ModbusNumberException, ModbusIOException
{
int[] registerValues = modbusMaster.readHoldingRegisters(slaveId, register, 2);
return registerValues[0] << 16 | registerValues[1];
}
static void sleep(int dur_ms)
{
try
{
Thread.sleep(dur_ms);
}
catch (InterruptedException ex)
{
System.getLogger(KebaModbus.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
}
}
/**
* MODBUS-Register lesen und Wert in Map speichern
* @param name Name, unter dem der Wert gespeichert wird
* @param register
* @param slaveId
* @param pause_ms Pause vor Abfrage
* @return gelesener Wert
* @throws ModbusProtocolException
* @throws ModbusNumberException
* @throws ModbusIOException
*/
int read(String name, int register, int slaveId, int pause_ms) throws ModbusProtocolException, ModbusNumberException, ModbusIOException
{
sleep(pause_ms);
var val = readInt32BigEndian(getModbusMaster(), slaveId, register);
valueMap.put(name, val);
return val;
}
/**
* Testcode
* @param args the command line arguments: IP Startadr len [port]
*/
public static void main(String[] args)
{
if (args.length < 3)
{
usage();
System.exit(1);
}
int slaveId = 255; //1;
int offset = 1022; // Register-offset
int quantity = 5; // Anz. Bytes
String ipa = "192.168.0.199";
int port = Modbus.TCP_PORT; // default 502
// Parameterauswertung
try
{
ipa = args[0];
offset = Integer.parseInt(args[1]);
quantity = Integer.parseInt(args[2]);
if(args.length > 3)
port = Integer.parseInt(args[3]);
TcpParameters tcpParameters = new TcpParameters();
// TcpParameters tcpParametersSlave = new TcpParameters();
//tcp parameters have already set by default as in example
// tcpParameters.setHost(InetAddress.getLocalHost());
tcpParameters.setHost(InetAddress.getByName(ipa));
tcpParameters.setKeepAlive(true);
tcpParameters.setPort(port);
// tcpParametersSlave.setPort(Modbus.TCP_PORT);
ModbusMaster modbusMaster;
/*
// if you would like to set connection parameters separately,
// you should use another method: createModbusMasterTCP(String host, int port, boolean keepAlive);
m = ModbusMasterFactory.createModbusMasterTCP(tcpParameters);
Modbus.setAutoIncrementTransactionId(true);
// jaho
m.setResponseTimeout(60000);
*/
System.err.printf("try to connect to %s:%d ...", ipa, port);
// ! diese Methode kommt immer sofort zurück. Man muss Ergebnis prüfen!
modbusMaster = connect(tcpParameters, timeout_ms);
if (modbusMaster.isConnected())
System.err.printf("done\n");
else
{
modbusMaster.connect();
if (!modbusMaster.isConnected())
{
System.err.printf("FAILED\n");
System.exit(1);
}
}
try
{
// since 1.2.8
if (!modbusMaster.isConnected())
{
System.err.printf("retry connect to %s:%d ...\n", ipa, port);
modbusMaster.connect();
}
System.err.printf("connected to %s:%d, reading Holding Registers %d +%d\n", ipa, port, offset, quantity);
// Abfrage Holding-Regs
// at next string we receive x holdingRegisters from a slave with id of 1 at offset of 0.
int[] registerValues = modbusMaster.readHoldingRegisters(slaveId, offset, quantity);
// Ausgabe Adr (dezimal), Wort (dez), h-Byte (dez), L-Byte (dez), Wort(hex), h-Byte(hex), L-Byte(hex)
for (int value : registerValues)
{
System.out.printf("%04d:%05d:%03d:%03d:%04X:%02X:%02X\n", offset++, value, (value & 0xff00) / 256, value & 0xff, value, (value & 0xff00) / 256, value & 0xff);
}
// Dieser Teil schein nicht zu gehen
/*
// also since 1.2.8.4 you can create your own request and process it with the master
offset = 0;
ReadHoldingRegistersRequest request = new ReadHoldingRegistersRequest();
request.setServerAddress(1);
request.setStartAddress(offset);
request.setTransactionId(0);
ReadHoldingRegistersResponse response = (ReadHoldingRegistersResponse) m.processRequest(request);
// you can get either int[] containing register values or byte[] containing raw bytes.
System.out.println("-----------------------");
for (int value : response.getRegisters())
{
System.out.println("Address: " + offset++ + ", Value: " + value);
}
*/
sleep(600);
int register = 1000;
int val = readInt32BigEndian(modbusMaster, slaveId, register);
System.out.printf("Wert für reg %d: %d (0x%04x)\n", register, val, val);
sleep(600);
register = 1004;
val = readInt32BigEndian(modbusMaster, slaveId, register);
System.out.printf("Wert für reg %d: %d (0x%04x)\n", register, val, val);
sleep(600);
register = 1014;
val = readInt32BigEndian(modbusMaster, slaveId, register);
System.out.printf("Wert für reg %d (S/N): %d (0x%04x)\n", register, val, val);
sleep(600);
register = 1008;
val = readInt32BigEndian(modbusMaster, slaveId, register);
System.out.printf("Wert für reg %d (current 1 [A]): %1.1f (0x%04x)\n", register, val/1000f, val);
sleep(600);
register = 1020;
val = readInt32BigEndian(modbusMaster, slaveId, register);
System.out.printf("Wert für reg %d (active Power): %1.1f kW (0x%04x)\n", register, val/1e6, val);
sleep(600);
register = 1500;
val = readInt32BigEndian(modbusMaster, slaveId, register);
System.out.printf("Wert für reg %d (RFID-Card): %d (0x%04x)\n", register, val, val);
sleep(600);
// angeblich Wh, aber in Wirklichkeit Wh/10
register = 1502;
val = readInt32BigEndian(modbusMaster, slaveId, register);
System.out.printf("Wert für reg %d (charged energy [kWh]): %1.1f (0x%04x)\n", register, val/10000f, val);
System.exit(0);
} catch (ModbusProtocolException e)
{
e.printStackTrace();
} catch (ModbusNumberException e)
{
e.printStackTrace();
}
catch (ModbusIOException e)
{
e.printStackTrace();
}
catch (NumberFormatException e)
{
System.err.println("Parameter ist keine Zahl: " + e.getMessage());
}
finally
{
try {
modbusMaster.disconnect();
} catch (ModbusIOException e) {
e.printStackTrace();
}
System.exit(1);
}
} catch (RuntimeException e)
{
throw e;
} catch (Exception e)
{
e.printStackTrace();
}
finally
{
System.exit(1);
}
System.exit(0);
} // main
/**
* MODBUS-Wert aus cache liefern oder neu abfragen, wenn maxAgeMinutes überschritten ist
* @param jName
* @param register
* @param slaveId
* @param pause_ms
* @param maxAgeMinutes
* @return
* @throws ModbusIOException
* @throws ModbusProtocolException
* @throws ModbusNumberException
*/
int readCached(String jName, int register, int slaveId, int pause_ms, int maxAgeMinutes) throws ModbusIOException, ModbusProtocolException, ModbusNumberException
{
// test, ob cache existiert
if (cache.containsKey(jName))
{
// noch gültig?
if (cache.get(jName).ts.plusMinutes(maxAgeMinutes).isAfter(LocalDateTime.now()))
return cache.get(jName).value;
}
int intVal = read(jName, register, slaveId, pause_ms);
cache.put(jName, new RCacheEntry(jName, LocalDateTime.now(), intVal));
return intVal;
}
}
+229
View File
@@ -0,0 +1,229 @@
/*
*/
package kebaagent;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpHandlers;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.Request;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.Optional;
import java.util.function.Predicate;
/**
*
* @author holz
*/
public class Webserver
{
private final int port;
private KebaAgent agent;
/**
* call start()
* @see #start()
* @param port
*/
public Webserver(int port)
{
this.port = port;
}
/**
* Start Webserver
* @param agent
* @throws IOException
*/
public void start(KebaAgent agent) throws IOException
{
this.agent = agent;
// Create an HttpServer instance on port
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
String defaultUri = "/keba";
Optional<String> pv = agent.getPropValue("webserver", "defaultUri");
if (pv.isPresent())
defaultUri = pv.get();
// Define a context that serves files from the current directory
// server.createContext("/", new com.sun.net.httpserver.HttpHandler()
// {
// @Override
// public void handle(com.sun.net.httpserver.HttpExchange exchange) throws IOException
// {
// // Handle requests here (simple file serving)
// System.out.println("recvd: " + exchange);
//// TODO: müsste jetzt die Response generieren. Wie genau, noch nicht nachvollzogen
//// ein komplexeres Bsp unter https://dzone.com/articles/simple-http-server-in-java
//// Daraus stammt die Klasse MyHttpHandler unten
// }
// });
// andere Variante für Handler am Bsp url http://localhost:8000/t/allowed oder http://localhost:8000/t<irgendwas anderes>
// Headers headers = new Headers();
// headers.set("Allow", "GET");
// headers.set("Content-Type", "text/plain");
//
// HttpHandler allowedResponse = HttpHandlers.of(200, headers, getBody()); // Statuscode, Headers, body
// Trivialhandler für einfache Antwort
HttpHandler deniedResponse = HttpHandlers.of(401, Headers.of("Deny", "GET"), "Denied");
JsonHandler allowedResponse = new JsonHandler(agent);
// welche URL ist erlaubt?
final String fDefUri = defaultUri;
Predicate<Request> findAllowedPath = r ->
// r.getRequestURI().getPath().equals("/t/allowed");
r.getRequestURI().getPath().equals(fDefUri);
HttpHandler handler = HttpHandlers.handleOrElse(findAllowedPath, allowedResponse, deniedResponse);
// context ist weiter oben angebunden (ab /t)
// server.createContext("/t", handler);
server.createContext(defaultUri, handler);
// ggf. 2. Handler für / bauen (Infoseite)
// mit MyHttpHandler, ziemlich buggy und unvollständig von https://dzone.com/articles/simple-http-server-in-java
// MyHttpHandler myHandler = new MyHttpHandler(agent);
// server.createContext("/m", myHandler);
// Start the server
server.start();
System.out.printf("Server started on port %d\n", port);
}
// private String getBody()
// {
// return "{ 'info': 'hello'}";
// }
//
private static class MyHttpHandler implements HttpHandler
{
private KebaAgent agent;
private MyHttpHandler(KebaAgent a)
{
agent = a;
}
/**
* zu implementierende Methode
* @param httpExchange
* @throws IOException
*/
@Override
public void handle(HttpExchange httpExchange) throws IOException
{
String requestParamValue = null;
System.out.println("MyHttpHandler.handle() httpExchange " + httpExchange);
String requestMethod = httpExchange.getRequestMethod();
System.out.println("MyHttpHandler.handle().getRequestMethod(): " + requestMethod);
if ("GET".equals(httpExchange.getRequestMethod()))
{
requestParamValue = handleGetRequest(httpExchange);
System.out.println("MyHttpHandler.handle() GET: requestParamValue = " + requestParamValue);
}
// TODO: nicht im Bsp.
// else if("POST".equals(httpExchange))
// {
// requestParamValue = handlePostRequest(httpExchange);
// }
handleResponse(httpExchange, requestParamValue);
}
/**
* hier hat der Autor Schwachsinn produziert
* @param httpExchange
* @return
*/
private String handleGetRequest(HttpExchange httpExchange)
{
String path = httpExchange.getRequestURI().toString();
try
{
// return path.split("\\?")[1]
return path.split("/")[1]
// .split("=")[1];
;
} catch (Exception e)
{
e.printStackTrace();
return e.getMessage();
}
}
private void handleResponse(HttpExchange httpExchange, String requestParamValue) throws IOException
{
System.out.println("MyHttpHandler.handleResponse ");
OutputStream outputStream = httpExchange.getResponseBody();
StringBuilder htmlBuilder = new StringBuilder();
htmlBuilder
.append("<html>")
.append("<body>")
.append("<h1>")
.append("Hello ")
.append(requestParamValue)
.append("</h1>")
.append("</body>")
.append("</html>");
// encode HTML content
// apache commons text + lang
try
{
// Schwachsinn: Anwendung auf den ganzen HTML-Code
// String htmlResponse = StringEscapeUtils.escapeHtml4(htmlBuilder.toString());
// So OK
String htmlResponse = htmlBuilder.toString();
// jaho: Header erweitern
Headers responseHeaders = httpExchange.getResponseHeaders();
responseHeaders.set("Content-Type", "text/plain");
// this line is a must (jaho: header senden)
httpExchange.sendResponseHeaders(200, htmlResponse.length());
outputStream.write(htmlResponse.getBytes());
outputStream.flush();
outputStream.close();
} catch (Throwable e)
{
e.printStackTrace();
}
}
} // MyHttpHandler
}