diff --git a/hal-core/build.gradle b/hal-core/build.gradle index a0811c12..a71f9032 100644 --- a/hal-core/build.gradle +++ b/hal-core/build.gradle @@ -7,3 +7,20 @@ dependencies { implementation 'org.shredzone.acme4j:acme4j-client:2.12' implementation 'org.shredzone.acme4j:acme4j-utils:2.12' } + +// Make test classes available to other projects + +configurations { + testClasses { + extendsFrom(testImplementation) + } +} + +task testJar(type: Jar) { + archiveClassifier.set('test') + from sourceSets.test.output +} + +artifacts { + testClasses testJar // add the jar generated by the testJar task to the testClasses dependency +} \ No newline at end of file diff --git a/hal-core/resources/web/plugin_config.tmpl b/hal-core/resources/web/plugin_config.tmpl index e4068ea9..e2f5375a 100644 --- a/hal-core/resources/web/plugin_config.tmpl +++ b/hal-core/resources/web/plugin_config.tmpl @@ -83,7 +83,7 @@ -
+
Daemons
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 getDeviceControllerClass() { + return HalMqttController.class; + } + + @Override + public Class 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 getDeviceControllerClass() { + return HalMqttController.class; + } + + @Override + public Class 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 getDeviceControllerClass() { - return HalMqttController.class; - } - - @Override - public Class 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