diff --git a/hal-core/src/se/hal/HalServer.java b/hal-core/src/se/hal/HalServer.java
index 5d45519b..50aeb924 100644
--- a/hal-core/src/se/hal/HalServer.java
+++ b/hal-core/src/se/hal/HalServer.java
@@ -1,6 +1,5 @@
package se.hal;
-
import se.hal.daemon.HalExternalWebDaemon;
import se.hal.intf.*;
import se.hal.intf.HalJavascriptModule.HalJsModule;
@@ -82,7 +81,7 @@ public class HalServer {
logger.info("Looking for plugins.");
// Disable plugins based on settings
- for (PluginData plugin : getAllPlugins()) {
+ for (PluginData plugin : (List
) pluginManager.getAllPlugins()) {
PluginConfig pluginConfig = PluginConfig.getPluginConfig(db, plugin.getName());
// If plugin is not found in DB then disable it and create an entry.
@@ -177,17 +176,13 @@ public class HalServer {
logger.info("Plugin '" + name + "' has been " + (enabled ? "enabled" : "disabled") + ", change will take affect after restart.");
pluginConfig.setEnabled(enabled);
- pluginManager.getPluginData(name).setEnabled(pluginConfig.isEnabled());
+ pluginManager.getPlugin(name).setEnabled(pluginConfig.isEnabled());
pluginConfig.save(db);
}
- public static List getEnabledPlugins() {
- return pluginManager.toArray();
- }
-
- public static List getAllPlugins() {
- return pluginManager.toArrayAll();
+ public static PluginManager> getPluginManager() {
+ return pluginManager;
}
public static List getControllerManagers() {
diff --git a/hal-core/src/se/hal/intf/HalDeviceData.java b/hal-core/src/se/hal/intf/HalDeviceData.java
index 6cd6e23c..af071645 100644
--- a/hal-core/src/se/hal/intf/HalDeviceData.java
+++ b/hal-core/src/se/hal/intf/HalDeviceData.java
@@ -22,4 +22,14 @@ public abstract class HalDeviceData {
public abstract double getData();
public abstract void setData(double data);
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ HalDeviceData that = (HalDeviceData) o;
+ return getTimestamp() == that.getTimestamp() &&
+ getData() == that.getData();
+ }
}
diff --git a/hal-core/src/se/hal/intf/HalEventData.java b/hal-core/src/se/hal/intf/HalEventData.java
index a3f646f6..3b7962dd 100644
--- a/hal-core/src/se/hal/intf/HalEventData.java
+++ b/hal-core/src/se/hal/intf/HalEventData.java
@@ -3,6 +3,6 @@ package se.hal.intf;
/**
* Interface representing one report from an event
*/
-public abstract class HalEventData extends HalDeviceData{
+public abstract class HalEventData extends HalDeviceData {
}
diff --git a/hal-core/src/se/hal/page/PluginConfigWebPage.java b/hal-core/src/se/hal/page/PluginConfigWebPage.java
index 43a95b8b..3d96e0ca 100644
--- a/hal-core/src/se/hal/page/PluginConfigWebPage.java
+++ b/hal-core/src/se/hal/page/PluginConfigWebPage.java
@@ -77,7 +77,7 @@ public class PluginConfigWebPage extends HalWebPage {
}
Templator tmpl = new Templator(FileUtil.find(TEMPLATE));
- tmpl.set("plugins", HalServer.getAllPlugins());
+ tmpl.set("plugins", HalServer.getPluginManager().getAllPlugins());
tmpl.set("controllers", HalAbstractControllerManager.getControllers());
tmpl.set("daemons", HalServer.getAllDaemons());
return tmpl;
diff --git a/hal-core/test/se/hal/test/MockHalDeviceReportListener.java b/hal-core/test/se/hal/test/MockHalDeviceReportListener.java
new file mode 100644
index 00000000..48c25592
--- /dev/null
+++ b/hal-core/test/se/hal/test/MockHalDeviceReportListener.java
@@ -0,0 +1,45 @@
+package se.hal.test;
+
+import se.hal.intf.HalDeviceConfig;
+import se.hal.intf.HalDeviceData;
+import se.hal.intf.HalDeviceReportListener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class will store all reports to be used later in a test to verify the behaviours
+ */
+public class MockHalDeviceReportListener implements HalDeviceReportListener {
+
+ private List reports = new ArrayList<>();
+
+
+ public int getNumberOfReports() {
+ return reports.size();
+ }
+
+ public DeviceTestReport getReport(int index) {
+ return reports.get(index);
+ }
+
+ public void reset() {
+ reports = new ArrayList<>();
+ }
+
+ @Override
+ public void reportReceived(HalDeviceConfig deviceConfig, HalDeviceData deviceData) {
+ reports.add(new DeviceTestReport(deviceConfig, deviceData));
+ }
+
+
+ public static class DeviceTestReport {
+ public HalDeviceConfig config;
+ public HalDeviceData data;
+
+ private DeviceTestReport(HalDeviceConfig config, HalDeviceData data) {
+ this.config = config;
+ this.data = data;
+ }
+ }
+}
diff --git a/plugins/hal-mqtt/build.gradle b/plugins/hal-mqtt/build.gradle
index 81fb360f..c388f552 100644
--- a/plugins/hal-mqtt/build.gradle
+++ b/plugins/hal-mqtt/build.gradle
@@ -1,3 +1,5 @@
dependencies {
implementation project(':hal-core')
+
+ testImplementation project(path: ':hal-core', configuration: 'testClasses')
}
diff --git a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/HalMqttController.java b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/HalMqttController.java
index a86f198b..c545e010 100644
--- a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/HalMqttController.java
+++ b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/HalMqttController.java
@@ -24,10 +24,11 @@
package se.hal.plugin.mqtt;
+import se.hal.HalServer;
import se.hal.daemon.HalMulticastDnsDaemon;
import se.hal.intf.*;
+import se.hal.plugin.mqtt.detector.HalMqttDetector;
import se.hal.plugin.mqtt.device.HalMqttDeviceConfig;
-import se.hal.plugin.mqtt.device.HalMqttUnknownDeviceConfig;
import zutil.InetUtil;
import zutil.ObjectUtil;
import zutil.log.LogUtil;
@@ -37,10 +38,7 @@ import zutil.net.mqtt.MqttSubscriptionListener;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
+import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -50,6 +48,7 @@ public class HalMqttController implements HalAutostartController, MqttSubscripti
private MqttBroker mqttBroker;
+ private List detectors = Collections.emptyList();
private HashMap> topics = new HashMap<>();
private List deviceListeners = new CopyOnWriteArrayList<>();
@@ -73,6 +72,8 @@ public class HalMqttController implements HalAutostartController, MqttSubscripti
mqttBroker.addGlobalSubscriber(this);
mqttBroker.start();
+ detectors = HalServer.getPluginManager().getEnabledPluginSingletons(HalMqttDetector.class);
+
} catch (IOException e) {
logger.log(Level.SEVERE, "Unable to initialize MQTT plugin.", e);
@@ -80,11 +81,6 @@ public class HalMqttController implements HalAutostartController, MqttSubscripti
}
}
- @Override
- public boolean isAvailable() {
- return true;
- }
-
@Override
public void close(){
if (mqttBroker != null) {
@@ -102,16 +98,40 @@ public class HalMqttController implements HalAutostartController, MqttSubscripti
public void dataPublished(String topic, byte[] data) {
logger.finest("MQTT data published(topic: " + topic + "): " + new String(data, StandardCharsets.UTF_8));
- List devices = topics.get(topic);
- if (ObjectUtil.isEmpty(devices))
- devices = Arrays.asList(new HalMqttUnknownDeviceConfig(topic, data));
+ List registeredDevices = topics.get(topic);
- for (HalMqttDeviceConfig deviceConfig : devices) {
- HalDeviceData deviceData = deviceConfig.getDeviceData(data);
+ // Handle existing devices
- if (deviceListeners != null) {
- for (HalDeviceReportListener deviceListener : deviceListeners) {
- deviceListener.reportReceived(deviceConfig, deviceData);
+ if (!ObjectUtil.isEmpty(registeredDevices)) {
+ for (HalMqttDeviceConfig deviceConfig : registeredDevices) {
+ HalDeviceData deviceData = deviceConfig.getDeviceData(data);
+
+ if (deviceListeners != null) {
+ for (HalDeviceReportListener deviceListener : deviceListeners) {
+ deviceListener.reportReceived(deviceConfig, deviceData);
+ }
+ }
+ }
+ }
+
+ // Handle new devices
+
+ for (HalMqttDetector detector : detectors) {
+ List detectedDevices = detector.parseTopic(topic, data);
+
+ // Check if we already know the device
+ if (!ObjectUtil.isEmpty(detectedDevices)) {
+ for (HalMqttDeviceConfig detectedDeviceConfig : detectedDevices) {
+ // Only handle unknown devices
+ if (!registeredDevices.contains(detectedDeviceConfig)) {
+ HalDeviceData deviceData = detectedDeviceConfig.getDeviceData(data);
+
+ if (deviceListeners != null) {
+ for (HalDeviceReportListener deviceListener : deviceListeners) {
+ deviceListener.reportReceived(detectedDeviceConfig, deviceData);
+ }
+ }
+ }
}
}
}
@@ -153,7 +173,8 @@ public class HalMqttController implements HalAutostartController, MqttSubscripti
if (eventConfig instanceof HalMqttDeviceConfig) {
HalMqttDeviceConfig mqttEvent = (HalMqttDeviceConfig) eventConfig;
mqttBroker.publish(mqttEvent.getTopic(), Double.toString(eventData.getData()).getBytes());
- } else throw new IllegalArgumentException(
+ } else
+ throw new IllegalArgumentException(
"Device config is not an instance of " + HalMqttDeviceConfig.class + ": " + eventConfig.getClass());
}
diff --git a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/GenericMqttDetector.java b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/GenericMqttDetector.java
new file mode 100644
index 00000000..229457af
--- /dev/null
+++ b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/GenericMqttDetector.java
@@ -0,0 +1,78 @@
+package se.hal.plugin.mqtt.detector;
+
+import se.hal.plugin.mqtt.device.HalMqttDeviceConfig;
+import se.hal.plugin.mqtt.device.HalMqttHumidityDeviceConfig;
+import se.hal.plugin.mqtt.device.HalMqttParticularMatterDeviceConfig;
+import se.hal.plugin.mqtt.device.HalMqttTemperatureDeviceConfig;
+import zutil.ObjectUtil;
+import zutil.parser.DataNode;
+import zutil.parser.json.JSONParser;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This detector will handle any MQTT topics and messages that have a generic structure.
+ * Supported formats:
+ *
+ * Topics ending with: temperature, humidity, ppm
+ * Topics where the payload is a JSON object containing the fields: temperature, humidity, ppm
+ *
+ */
+public class GenericMqttDetector implements HalMqttDetector {
+
+ @Override
+ public List parseTopic(String topic, byte[] data) {
+ if (ObjectUtil.isEmpty(topic))
+ return Collections.emptyList();
+
+ List detectedDeviceConfigs = new ArrayList<>();
+ String[] topicSections = topic.split("/");
+
+ // Check if the topic ends with specific keywords
+
+ if (topicSections.length > 1) {
+ String keyword = topicSections[topicSections.length - 1];
+ HalMqttDeviceConfig config = null;
+
+ switch (keyword) {
+ case "temperature":
+ config = new HalMqttTemperatureDeviceConfig(topic);
+ break;
+ case "humidity":
+ config = new HalMqttHumidityDeviceConfig(topic);
+ break;
+ case "pm25":
+ config = new HalMqttParticularMatterDeviceConfig(topic);
+ break;
+ }
+
+ if (config != null) {
+ detectedDeviceConfigs.add(config);
+ return null;
+ }
+ }
+
+ // Check if the payload is in JSON format
+
+ DataNode jsonPayload = JSONParser.read(new String(data, StandardCharsets.UTF_8));
+ if (jsonPayload != null) {
+ if (jsonPayload.get("temperature") != null) {
+ HalMqttDeviceConfig config = new HalMqttTemperatureDeviceConfig(topic, "$.temperature");
+ detectedDeviceConfigs.add(config);
+ }
+ if (jsonPayload.get("humidity") != null) {
+ HalMqttDeviceConfig config = new HalMqttHumidityDeviceConfig(topic, "$.humidity");
+ detectedDeviceConfigs.add(config);
+ }
+ if (jsonPayload.get("pm25") != null) {
+ HalMqttDeviceConfig config = new HalMqttParticularMatterDeviceConfig(topic, "$.pm25");
+ detectedDeviceConfigs.add(config);
+ }
+ }
+
+ return detectedDeviceConfigs;
+ }
+}
diff --git a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/HalMqttDetector.java b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/HalMqttDetector.java
new file mode 100644
index 00000000..6055cace
--- /dev/null
+++ b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/HalMqttDetector.java
@@ -0,0 +1,15 @@
+package se.hal.plugin.mqtt.detector;
+
+import se.hal.plugin.mqtt.device.HalMqttDeviceConfig;
+
+import java.util.List;
+
+/**
+ * This interface defines the interface required by a MQTT Device detector.
+ * The purpose of the implementing classes is to simplify creation of devices based on
+ * MQTT by automatically detecting and parsing devices from reported topics.
+ */
+public interface HalMqttDetector {
+
+ List parseTopic(String topic, byte[] data);
+}
diff --git a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/TasmotaMqttDetector.java b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/TasmotaMqttDetector.java
new file mode 100644
index 00000000..88740c81
--- /dev/null
+++ b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/TasmotaMqttDetector.java
@@ -0,0 +1,26 @@
+package se.hal.plugin.mqtt.detector;
+
+import se.hal.intf.HalDeviceConfig;
+import se.hal.intf.HalDeviceData;
+import se.hal.intf.HalDeviceReportListener;
+import se.hal.plugin.mqtt.device.HalMqttDeviceConfig;
+import zutil.ObjectUtil;
+
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class TasmotaMqttDetector implements HalMqttDetector {
+
+
+ @Override
+ public List parseTopic(String topic, byte[] data) {
+ if (ObjectUtil.isEmpty(topic) ||
+ ! topic.startsWith("zigbee2mqtt/") ||
+ topic.startsWith("zigbee2mqtt/bridge/"))
+ return Collections.emptyList();
+
+ List detectedDeviceConfigs = new ArrayList<>();
+ return detectedDeviceConfigs;
+ }
+
+}
diff --git a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/Zigbee2mqttDetector.java b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/Zigbee2mqttDetector.java
new file mode 100644
index 00000000..8cbace57
--- /dev/null
+++ b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/detector/Zigbee2mqttDetector.java
@@ -0,0 +1,24 @@
+package se.hal.plugin.mqtt.detector;
+
+import se.hal.intf.HalDeviceConfig;
+import se.hal.intf.HalDeviceData;
+import se.hal.intf.HalDeviceReportListener;
+import se.hal.plugin.mqtt.device.HalMqttDeviceConfig;
+import zutil.ObjectUtil;
+
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class Zigbee2mqttDetector implements HalMqttDetector {
+
+ @Override
+ public List parseTopic(String topic, byte[] data) {
+ if (ObjectUtil.isEmpty(topic) ||
+ ! topic.startsWith("zigbee2mqtt/") ||
+ topic.startsWith("zigbee2mqtt/bridge/"))
+ return Collections.emptyList();
+
+ List detectedDeviceConfigs = new ArrayList<>();
+ return detectedDeviceConfigs;
+ }
+}
diff --git a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttDeviceConfig.java b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttDeviceConfig.java
index a422ad4d..f9c06d01 100644
--- a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttDeviceConfig.java
+++ b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttDeviceConfig.java
@@ -57,6 +57,7 @@ import zutil.ui.conf.Configurator;
import java.util.Objects;
public abstract class HalMqttDeviceConfig implements HalSensorConfig {
+
@Configurator.Configurable(value = "MQTT Topic")
private String topic;
@Configurator.Configurable(value = "JSON Path", description = "If the value of the topic is a JSON then this parameter can be used to specify the path to the e.g. temperature value." +
@@ -64,6 +65,25 @@ public abstract class HalMqttDeviceConfig implements HalSensorConfig {
private String jsonPath;
+ public HalMqttDeviceConfig() {}
+
+ /**
+ * @param topic is the topic associated to this device
+ */
+ public HalMqttDeviceConfig(String topic) {
+ this.topic = topic;
+ }
+
+ /**
+ * @param topic is the topic associated to this device.
+ * @param jsonPath indicates that the payload is of JSON format and the data should be extracted from this path.
+ */
+ public HalMqttDeviceConfig(String topic, String jsonPath) {
+ this.topic = topic;
+ this.jsonPath = jsonPath;
+ }
+
+
public String getTopic() {
return topic;
}
diff --git a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttHumidityDeviceConfig.java b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttHumidityDeviceConfig.java
new file mode 100644
index 00000000..d182fda0
--- /dev/null
+++ b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttHumidityDeviceConfig.java
@@ -0,0 +1,106 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2020 Ziver Koc
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2020 Ziver Koc
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package se.hal.plugin.mqtt.device;
+
+import se.hal.intf.HalDeviceData;
+import se.hal.intf.HalEventController;
+import se.hal.plugin.mqtt.HalMqttController;
+import se.hal.struct.devicedata.HumiditySensorData;
+import zutil.ObjectUtil;
+import zutil.converter.Converter;
+import zutil.parser.DataNode;
+import zutil.parser.DataNodePath;
+import zutil.parser.json.JSONParser;
+
+import java.nio.charset.StandardCharsets;
+
+public class HalMqttHumidityDeviceConfig extends HalMqttDeviceConfig {
+
+ public HalMqttHumidityDeviceConfig() {}
+ public HalMqttHumidityDeviceConfig(String topic) {
+ super(topic);
+ }
+ public HalMqttHumidityDeviceConfig(String topic, String jsonPath) {
+ super(topic, jsonPath);
+ }
+
+ // --------------------------
+ // Hal Methods
+ // --------------------------
+
+ @Override
+ public Class extends HalEventController> getDeviceControllerClass() {
+ return HalMqttController.class;
+ }
+
+ @Override
+ public Class extends HalDeviceData> getDeviceDataClass() {
+ return HumiditySensorData.class;
+ }
+
+ @Override
+ public HumiditySensorData getDeviceData(byte[] data) {
+ if (!ObjectUtil.isEmpty(getJsonPath())) {
+ String dataStr = new String(data, StandardCharsets.UTF_8);
+ DataNode json = JSONParser.read(dataStr);
+ DataNode deviceDataValue = DataNodePath.search(getJsonPath(), json);
+
+ if (deviceDataValue != null)
+ return new HumiditySensorData(deviceDataValue.getDouble(), System.currentTimeMillis());
+ else
+ return null;
+ }
+ return new HumiditySensorData(Converter.toInt(data), System.currentTimeMillis());
+ }
+
+ @Override
+ public AggregationMethod getAggregationMethod() {
+ return AggregationMethod.AVERAGE;
+ }
+}
diff --git a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttParticularMatterDeviceConfig.java b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttParticularMatterDeviceConfig.java
index ae908ffb..ae9534a3 100644
--- a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttParticularMatterDeviceConfig.java
+++ b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttParticularMatterDeviceConfig.java
@@ -62,6 +62,14 @@ import java.nio.charset.StandardCharsets;
public class HalMqttParticularMatterDeviceConfig extends HalMqttDeviceConfig {
+ public HalMqttParticularMatterDeviceConfig() {}
+ public HalMqttParticularMatterDeviceConfig(String topic) {
+ super(topic);
+ }
+ public HalMqttParticularMatterDeviceConfig(String topic, String jsonPath) {
+ super(topic, jsonPath);
+ }
+
// --------------------------
// Hal Methods
// --------------------------
@@ -77,7 +85,7 @@ public class HalMqttParticularMatterDeviceConfig extends HalMqttDeviceConfig {
}
@Override
- public HalDeviceData getDeviceData(byte[] data) {
+ public ParticulateMatterSensorData getDeviceData(byte[] data) {
if (!ObjectUtil.isEmpty(getJsonPath())) {
String dataStr = new String(data, StandardCharsets.UTF_8);
DataNode json = JSONParser.read(dataStr);
diff --git a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttTemperatureDeviceConfig.java b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttTemperatureDeviceConfig.java
new file mode 100644
index 00000000..02b103f1
--- /dev/null
+++ b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttTemperatureDeviceConfig.java
@@ -0,0 +1,106 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2020 Ziver Koc
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2020 Ziver Koc
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package se.hal.plugin.mqtt.device;
+
+import se.hal.intf.HalDeviceData;
+import se.hal.intf.HalEventController;
+import se.hal.plugin.mqtt.HalMqttController;
+import se.hal.struct.devicedata.TemperatureSensorData;
+import zutil.ObjectUtil;
+import zutil.converter.Converter;
+import zutil.parser.DataNode;
+import zutil.parser.DataNodePath;
+import zutil.parser.json.JSONParser;
+
+import java.nio.charset.StandardCharsets;
+
+public class HalMqttTemperatureDeviceConfig extends HalMqttDeviceConfig {
+
+ public HalMqttTemperatureDeviceConfig() {}
+ public HalMqttTemperatureDeviceConfig(String topic) {
+ super(topic);
+ }
+ public HalMqttTemperatureDeviceConfig(String topic, String jsonPath) {
+ super(topic, jsonPath);
+ }
+
+ // --------------------------
+ // Hal Methods
+ // --------------------------
+
+ @Override
+ public Class extends HalEventController> getDeviceControllerClass() {
+ return HalMqttController.class;
+ }
+
+ @Override
+ public Class extends HalDeviceData> getDeviceDataClass() {
+ return TemperatureSensorData.class;
+ }
+
+ @Override
+ public TemperatureSensorData getDeviceData(byte[] data) {
+ if (!ObjectUtil.isEmpty(getJsonPath())) {
+ String dataStr = new String(data, StandardCharsets.UTF_8);
+ DataNode json = JSONParser.read(dataStr);
+ DataNode deviceDataValue = DataNodePath.search(getJsonPath(), json);
+
+ if (deviceDataValue != null)
+ return new TemperatureSensorData(deviceDataValue.getDouble(), System.currentTimeMillis());
+ else
+ return null;
+ }
+ return new TemperatureSensorData(Converter.toInt(data), System.currentTimeMillis());
+ }
+
+ @Override
+ public AggregationMethod getAggregationMethod() {
+ return AggregationMethod.AVERAGE;
+ }
+}
diff --git a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttUnknownDeviceConfig.java b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttUnknownDeviceConfig.java
deleted file mode 100644
index 21c771a9..00000000
--- a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/device/HalMqttUnknownDeviceConfig.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * The MIT License (MIT)
- *
- * Copyright (c) 2023 Ziver Koc
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-package se.hal.plugin.mqtt.device;
-
-import se.hal.intf.HalDeviceData;
-import se.hal.intf.HalEventController;
-import se.hal.plugin.mqtt.HalMqttController;
-import se.hal.struct.devicedata.TemperatureSensorData;
-
-import java.nio.charset.StandardCharsets;
-
-/**
- * Represents an unknown device data type
- */
-public class HalMqttUnknownDeviceConfig extends HalMqttDeviceConfig {
-
- /** Save data so it can be provided to user for analysis, the data will not be used in any other way */
- transient String data;
-
-
- public HalMqttUnknownDeviceConfig(String topic, byte[] data) {
- setTopic(topic);
- this.data = new String(data, StandardCharsets.UTF_8);
- }
-
- // --------------------------
- // Hal Methods
- // --------------------------
-
- @Override
- public Class extends HalEventController> getDeviceControllerClass() {
- return HalMqttController.class;
- }
-
- @Override
- public Class extends HalDeviceData> getDeviceDataClass() {
- return TemperatureSensorData.class; // Just return any data class just so we do not get so many error logs
- }
-
- @Override
- public HalDeviceData getDeviceData(byte[] data) {
- return null;
- }
-
- @Override
- public AggregationMethod getAggregationMethod() {
- return AggregationMethod.AVERAGE;
- }
-
- @Override
- public String toString() {
- return "Topic: " + getTopic() + ", Data: " + data;
- }
-}
diff --git a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/plugin.json b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/plugin.json
index d6f5a1fe..b778fc2e 100644
--- a/plugins/hal-mqtt/src/se/hal/plugin/mqtt/plugin.json
+++ b/plugins/hal-mqtt/src/se/hal/plugin/mqtt/plugin.json
@@ -4,7 +4,13 @@
"interfaces": [
{"se.hal.intf.HalAutostartController": "se.hal.plugin.mqtt.HalMqttController"},
+ {"se.hal.plugin.mqtt.detector.HalMqttDetector": "se.hal.plugin.mqtt.detector.GenericMqttDetector"},
+ {"se.hal.plugin.mqtt.detector.HalMqttDetector": "se.hal.plugin.mqtt.detector.TasmotaMqttDetector"},
+ {"se.hal.plugin.mqtt.detector.HalMqttDetector": "se.hal.plugin.mqtt.detector.Zigbee2mqttDetector"},
+
+ {"se.hal.intf.HalSensorConfig": "se.hal.plugin.mqtt.device.HalMqttHumidityDeviceConfig"},
{"se.hal.intf.HalSensorConfig": "se.hal.plugin.mqtt.device.HalMqttParticularMatterDeviceConfig"},
+ {"se.hal.intf.HalSensorConfig": "se.hal.plugin.mqtt.device.HalMqttTemperatureDeviceConfig"},
{"se.hal.intf.HalWebPage": "se.hal.plugin.mqtt.page.MqttOverviewPage"}
]
diff --git a/plugins/hal-mqtt/test/se/hal/plugin/mqtt/detector/GenericMqttDetectorTest.java b/plugins/hal-mqtt/test/se/hal/plugin/mqtt/detector/GenericMqttDetectorTest.java
new file mode 100644
index 00000000..7b1bfa38
--- /dev/null
+++ b/plugins/hal-mqtt/test/se/hal/plugin/mqtt/detector/GenericMqttDetectorTest.java
@@ -0,0 +1,141 @@
+package se.hal.plugin.mqtt.detector;
+
+import org.junit.Test;
+import se.hal.plugin.mqtt.device.HalMqttHumidityDeviceConfig;
+import se.hal.plugin.mqtt.device.HalMqttParticularMatterDeviceConfig;
+import se.hal.plugin.mqtt.device.HalMqttTemperatureDeviceConfig;
+import se.hal.struct.devicedata.HumiditySensorData;
+import se.hal.struct.devicedata.ParticulateMatterSensorData;
+import se.hal.struct.devicedata.TemperatureSensorData;
+import se.hal.test.MockHalDeviceReportListener;
+import zutil.converter.Converter;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.*;
+
+
+public class GenericMqttDetectorTest {
+
+ @Test
+ public void ignoredTopics() {
+ MockHalDeviceReportListener listener = new MockHalDeviceReportListener();
+ GenericMqttDetector detector = new GenericMqttDetector();
+ detector.addListener(listener);
+
+
+ listener.reset();
+ detector.parseTopic("", new byte[]{});
+ assertEquals(0, listener.getNumberOfReports());
+
+ listener.reset();
+ detector.parseTopic("invalid/topic", new byte[]{});
+ assertEquals(0, listener.getNumberOfReports());
+ }
+
+ @Test
+ public void parseTemperature() {
+ MockHalDeviceReportListener listener = new MockHalDeviceReportListener();
+ GenericMqttDetector detector = new GenericMqttDetector();
+ detector.addListener(listener);
+
+
+ listener.reset();
+ detector.parseTopic(
+ "zigbee2mqtt/Kitchen air quality/temperature",
+ Converter.toBytes(26));
+ assertEquals(1, listener.getNumberOfReports());
+ assertEquals(
+ new HalMqttTemperatureDeviceConfig("zigbee2mqtt/Kitchen air quality/temperature"),
+ listener.getReport(0).config);
+ listener.getReport(0).data.setTimestamp(0);
+ assertEquals(
+ new TemperatureSensorData(26, 0),
+ listener.getReport(0).data);
+
+
+ listener.reset();
+ detector.parseTopic(
+ "zigbee2mqtt/Kitchen air quality",
+ "{\"temperature\": 26}".getBytes(StandardCharsets.UTF_8));
+ assertEquals(1, listener.getNumberOfReports());
+ assertEquals(
+ new HalMqttTemperatureDeviceConfig("zigbee2mqtt/Kitchen air quality", "$.temperature"),
+ listener.getReport(0).config);
+ listener.getReport(0).data.setTimestamp(0);
+ assertEquals(
+ new TemperatureSensorData(26, 0),
+ listener.getReport(0).data);
+ }
+
+ @Test
+ public void parseHumidity() {
+ MockHalDeviceReportListener listener = new MockHalDeviceReportListener();
+ GenericMqttDetector detector = new GenericMqttDetector();
+ detector.addListener(listener);
+
+
+ listener.reset();
+ detector.parseTopic(
+ "zigbee2mqtt/Kitchen air quality/humidity",
+ Converter.toBytes(51));
+ assertEquals(1, listener.getNumberOfReports());
+ assertEquals(
+ new HalMqttHumidityDeviceConfig("zigbee2mqtt/Kitchen air quality/humidity"),
+ listener.getReport(0).config);
+ listener.getReport(0).data.setTimestamp(0);
+ assertEquals(
+ new HumiditySensorData(51, 0),
+ listener.getReport(0).data);
+
+
+ listener.reset();
+ detector.parseTopic(
+ "zigbee2mqtt/Kitchen air quality",
+ "{\"humidity\": 51}".getBytes(StandardCharsets.UTF_8));
+ assertEquals(1, listener.getNumberOfReports());
+ assertEquals(
+ new HalMqttHumidityDeviceConfig("zigbee2mqtt/Kitchen air quality", "$.humidity"),
+ listener.getReport(0).config);
+ listener.getReport(0).data.setTimestamp(0);
+ assertEquals(
+ new HumiditySensorData(51, 0),
+ listener.getReport(0).data);
+ }
+
+ @Test
+ public void parseParticularMatter() {
+ MockHalDeviceReportListener listener = new MockHalDeviceReportListener();
+ GenericMqttDetector detector = new GenericMqttDetector();
+ detector.addListener(listener);
+
+
+ listener.reset();
+ detector.parseTopic(
+ "zigbee2mqtt/Kitchen air quality/pm25",
+ Converter.toBytes(1));
+ assertEquals(1, listener.getNumberOfReports());
+ assertEquals(
+ new HalMqttParticularMatterDeviceConfig("zigbee2mqtt/Kitchen air quality/pm25"),
+ listener.getReport(0).config);
+ listener.getReport(0).data.setTimestamp(0);
+ assertEquals(
+ new ParticulateMatterSensorData(1, 0),
+ listener.getReport(0).data);
+
+
+ listener.reset();
+ detector.parseTopic(
+ "zigbee2mqtt/Kitchen air quality",
+ "{\"pm25\": 1}".getBytes(StandardCharsets.UTF_8));
+ assertEquals(1, listener.getNumberOfReports());
+ assertEquals(
+ new HalMqttParticularMatterDeviceConfig("zigbee2mqtt/Kitchen air quality", "$.pm25"),
+ listener.getReport(0).config);
+ listener.getReport(0).data.setTimestamp(0);
+ assertEquals(
+ new ParticulateMatterSensorData(1, 0),
+ listener.getReport(0).data);
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hal-mqtt/test/se/hal/plugin/mqtt/detector/Zigbee2mqttDetectorTest.java b/plugins/hal-mqtt/test/se/hal/plugin/mqtt/detector/Zigbee2mqttDetectorTest.java
new file mode 100644
index 00000000..dd6c6879
--- /dev/null
+++ b/plugins/hal-mqtt/test/se/hal/plugin/mqtt/detector/Zigbee2mqttDetectorTest.java
@@ -0,0 +1,62 @@
+package se.hal.plugin.mqtt.detector;
+
+import org.junit.Test;
+import se.hal.plugin.mqtt.device.HalMqttParticularMatterDeviceConfig;
+import se.hal.test.MockHalDeviceReportListener;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.*;
+
+
+public class Zigbee2mqttDetectorTest {
+
+ @Test
+ public void ignoredTopics() {
+ MockHalDeviceReportListener listener = new MockHalDeviceReportListener();
+ Zigbee2mqttDetector detector = new Zigbee2mqttDetector();
+ detector.addListener(listener);
+
+ listener.reset();
+ detector.parseTopic("", new byte[]{});
+ assertEquals(0, listener.getNumberOfReports());
+
+ listener.reset();
+ detector.parseTopic("invalid/topic", new byte[]{});
+ assertEquals(0, listener.getNumberOfReports());
+ }
+/*
+ @Test
+ public void parseTemperature() {
+ Zigbee2mqttDetector detector = new Zigbee2mqttDetector();
+
+ assertEquals(
+ Collections.emptyList(),
+ detector.parseTopic(
+ "zigbee2mqtt/Kitchen air quality",
+ "{\"temperature\":26}".getBytes(StandardCharsets.UTF_8)));
+ }
+
+ @Test
+ public void parseHumidity() {
+ Zigbee2mqttDetector detector = new Zigbee2mqttDetector();
+
+ assertEquals(
+ Collections.emptyList(),
+ detector.parseTopic(
+ "zigbee2mqtt/Kitchen air quality",
+ "{\"humidity\":51}".getBytes(StandardCharsets.UTF_8)));
+ }
+
+ @Test
+ public void parseParticularMatter() {
+ Zigbee2mqttDetector detector = new Zigbee2mqttDetector();
+
+ assertEquals(
+ Collections.list(new HalMqttParticularMatterDeviceConfig()),
+ detector.parseTopic(
+ "zigbee2mqtt/Kitchen air quality",
+ "{\"pm25\":1}".getBytes(StandardCharsets.UTF_8)));
+ }
+*/
+}
\ No newline at end of file