Introduced MQTT detectors to autodetect MQTT devices
This commit is contained in:
parent
e4707c2493
commit
b7ee6b16dc
21 changed files with 714 additions and 108 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
45
hal-core/test/se/hal/test/MockHalDeviceReportListener.java
Normal file
45
hal-core/test/se/hal/test/MockHalDeviceReportListener.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
dependencies {
|
||||
implementation project(':hal-core')
|
||||
|
||||
testImplementation project(path: ':hal-core', configuration: 'testClasses')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
*/
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue