Introduced MQTT detectors to autodetect MQTT devices

This commit is contained in:
Ziver Koc 2024-09-04 02:58:44 +02:00
parent e4707c2493
commit b7ee6b16dc
21 changed files with 714 additions and 108 deletions

View file

@ -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
}

View file

@ -83,7 +83,7 @@
</div>
</div>
<div class="col-md-12">
<div class="col-md-4">
<div class="panel panel-default drop-shadow">
<div class="panel-heading">Daemons</div>
<div class="panel-body">

View file

@ -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<PluginData>) 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<PluginData> getEnabledPlugins() {
return pluginManager.toArray();
}
public static List<PluginData> getAllPlugins() {
return pluginManager.toArrayAll();
public static PluginManager<?> getPluginManager() {
return pluginManager;
}
public static List<HalAbstractControllerManager> getControllerManagers() {

View file

@ -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();
}
}

View file

@ -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 {
}

View file

@ -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;

View file

@ -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<DeviceTestReport> 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;
}
}
}

View file

@ -1,3 +1,5 @@
dependencies {
implementation project(':hal-core')
testImplementation project(path: ':hal-core', configuration: 'testClasses')
}

View file

@ -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<HalMqttDetector> detectors = Collections.emptyList();
private HashMap<String, List<HalMqttDeviceConfig>> topics = new HashMap<>();
private List<HalDeviceReportListener> 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<HalMqttDeviceConfig> devices = topics.get(topic);
if (ObjectUtil.isEmpty(devices))
devices = Arrays.asList(new HalMqttUnknownDeviceConfig(topic, data));
List<HalMqttDeviceConfig> 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<HalMqttDeviceConfig> 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());
}

View file

@ -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:
* <u>
* <li>Topics ending with: temperature, humidity, ppm</li>
* <li>Topics where the payload is a JSON object containing the fields: temperature, humidity, ppm</li>
* </u>
*/
public class GenericMqttDetector implements HalMqttDetector {
@Override
public List<HalMqttDeviceConfig> parseTopic(String topic, byte[] data) {
if (ObjectUtil.isEmpty(topic))
return Collections.emptyList();
List<HalMqttDeviceConfig> 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;
}
}

View file

@ -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<HalMqttDeviceConfig> parseTopic(String topic, byte[] data);
}

View file

@ -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<HalMqttDeviceConfig> parseTopic(String topic, byte[] data) {
if (ObjectUtil.isEmpty(topic) ||
! topic.startsWith("zigbee2mqtt/") ||
topic.startsWith("zigbee2mqtt/bridge/"))
return Collections.emptyList();
List<HalMqttDeviceConfig> detectedDeviceConfigs = new ArrayList<>();
return detectedDeviceConfigs;
}
}

View file

@ -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<HalMqttDeviceConfig> parseTopic(String topic, byte[] data) {
if (ObjectUtil.isEmpty(topic) ||
! topic.startsWith("zigbee2mqtt/") ||
topic.startsWith("zigbee2mqtt/bridge/"))
return Collections.emptyList();
List<HalMqttDeviceConfig> detectedDeviceConfigs = new ArrayList<>();
return detectedDeviceConfigs;
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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"}
]

View file

@ -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);
}
}

View file

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