MQTT publish implementation, still without password.

This commit is contained in:
Admin
2016-11-16 16:58:11 -06:00
parent 90f2bce282
commit 68f38e1d95
23 changed files with 282 additions and 19 deletions

View File

@@ -5,7 +5,7 @@
<groupId>com.bwssystems.HABridge</groupId>
<artifactId>ha-bridge</artifactId>
<version>3.2.2c</version>
<version>3.2.2d</version>
<packaging>jar</packaging>
<name>HA Bridge</name>

View File

@@ -152,6 +152,7 @@ public class BridgeSettings extends BackupHandler {
theBridgeSettings.setNestConfigured(theBridgeSettings.isValidNest());
theBridgeSettings.setHueconfigured(theBridgeSettings.isValidHue());
theBridgeSettings.setHalconfigured(theBridgeSettings.isValidHal());
theBridgeSettings.setMqttconfigured(theBridgeSettings.isValidMQTT());
if(serverPortOverride != null)
theBridgeSettings.setServerPort(serverPortOverride);
setupParams(Paths.get(theBridgeSettings.getConfigfile()), ".cfgbk", "habridge.config-");

View File

@@ -77,7 +77,7 @@ public class HABridge {
//setup the mqtt handlers if available
mqttHome = new MQTTHome(bridgeSettings.getBridgeSettingsDescriptor());
// setup the class to handle the resource setup rest api
theResources = new DeviceResource(bridgeSettings.getBridgeSettingsDescriptor(), harmonyHome, nestHome, hueHome, halHome);
theResources = new DeviceResource(bridgeSettings.getBridgeSettingsDescriptor(), harmonyHome, nestHome, hueHome, halHome, mqttHome);
// setup the class to handle the upnp response rest api
theSettingResponder = new UpnpSettingsResource(bridgeSettings.getBridgeSettingsDescriptor());
theSettingResponder.setupServer();
@@ -88,7 +88,7 @@ public class HABridge {
}
else {
// setup the class to handle the hue emulator rest api
theHueMulator = new HueMulator(bridgeSettings.getBridgeSettingsDescriptor(), theResources.getDeviceRepository(), harmonyHome, nestHome, hueHome, udpSender);
theHueMulator = new HueMulator(bridgeSettings.getBridgeSettingsDescriptor(), theResources.getDeviceRepository(), harmonyHome, nestHome, hueHome, mqttHome, udpSender);
theHueMulator.setupServer();
// wait for the sparkjava initialization of the rest api classes to be complete
awaitInitialization();

View File

@@ -26,6 +26,7 @@ import com.bwssystems.harmony.HarmonyHome;
import com.bwssystems.hue.HueHome;
import com.bwssystems.luupRequests.Device;
import com.bwssystems.luupRequests.Scene;
import com.bwssystems.mqtt.MQTTHome;
import com.bwssystems.util.JsonTransformer;
import com.bwssystems.vera.VeraHome;
import com.google.gson.Gson;
@@ -42,9 +43,10 @@ public class DeviceResource {
private NestHome nestHome;
private HueHome hueHome;
private HalHome halHome;
private MQTTHome mqttHome;
private static final Set<String> supportedVerbs = new HashSet<>(Arrays.asList("get", "put", "post"));
public DeviceResource(BridgeSettingsDescriptor theSettings, HarmonyHome theHarmonyHome, NestHome aNestHome, HueHome aHueHome, HalHome aHalHome) {
public DeviceResource(BridgeSettingsDescriptor theSettings, HarmonyHome theHarmonyHome, NestHome aNestHome, HueHome aHueHome, HalHome aHalHome, MQTTHome aMqttHome) {
this.deviceRepository = new DeviceRepository(theSettings.getUpnpDeviceDb());
if(theSettings.isValidVera())
@@ -72,6 +74,11 @@ public class DeviceResource {
else
this.halHome = null;
if(theSettings.isValidMQTT())
this.mqttHome = aMqttHome;
else
this.mqttHome = null;
setupEndpoints();
}
@@ -280,6 +287,16 @@ public class DeviceResource {
return halHome.getDevices();
}, new JsonTransformer());
get (API_CONTEXT + "/mqtt/devices", "application/json", (request, response) -> {
log.debug("Get MQTT brokers");
if(mqttHome == null) {
response.status(HttpStatus.SC_NOT_FOUND);
return new ErrorMessage("A MQTT config is not available.");
}
response.status(HttpStatus.SC_OK);
return mqttHome.getBrokers();
}, new JsonTransformer());
// http://ip_address:port/api/devices/exec/renumber CORS request
options(API_CONTEXT + "/exec/renumber", "application/json", (request, response) -> {
response.status(HttpStatus.SC_OK);

View File

@@ -25,6 +25,9 @@ import com.bwssystems.hue.HueDeviceIdentifier;
import com.bwssystems.hue.HueErrorStringSet;
import com.bwssystems.hue.HueHome;
import com.bwssystems.hue.HueUtil;
import com.bwssystems.mqtt.MQTTHandler;
import com.bwssystems.mqtt.MQTTHome;
import com.bwssystems.mqtt.MQTTMessage;
import com.bwssystems.nest.controller.Nest;
import com.bwssystems.util.JsonTransformer;
import com.bwssystems.util.UDPDatagramSender;
@@ -92,6 +95,7 @@ public class HueMulator implements HueErrorStringSet {
private HarmonyHome myHarmonyHome;
private Nest theNest;
private HueHome myHueHome;
private MQTTHome mqttHome;
private HttpClient httpClient;
private CloseableHttpClient httpclientSSL;
private SSLContext sslcontext;
@@ -104,7 +108,7 @@ public class HueMulator implements HueErrorStringSet {
private String errorString;
public HueMulator(BridgeSettingsDescriptor theBridgeSettings, DeviceRepository aDeviceRepository, HarmonyHome theHarmonyHome, NestHome aNestHome, HueHome aHueHome, UDPDatagramSender aUdpDatagramSender) {
public HueMulator(BridgeSettingsDescriptor theBridgeSettings, DeviceRepository aDeviceRepository, HarmonyHome theHarmonyHome, NestHome aNestHome, HueHome aHueHome, MQTTHome aMqttHome, UDPDatagramSender aUdpDatagramSender) {
httpClient = HttpClients.createDefault();
// Trust own CA and all self-signed certs
sslcontext = SSLContexts.createDefault();
@@ -135,6 +139,10 @@ public class HueMulator implements HueErrorStringSet {
this.myHueHome = aHueHome;
else
this.myHueHome = null;
if(theBridgeSettings.isValidMQTT())
this.mqttHome = aMqttHome;
else
this.mqttHome = null;
bridgeSettings = theBridgeSettings;
theUDPDatagramSender = aUdpDatagramSender;
hueUser = null;
@@ -853,6 +861,44 @@ public class HueMulator implements HueErrorStringSet {
}
}
}
else if((device.getMapType() != null && device.getMapType().equalsIgnoreCase("mqttMessage")))
{
log.debug("executing HUE api request to send message to MQTT broker: " + url);
if(mqttHome != null)
{
if(url.substring(0, 1).equalsIgnoreCase("{")) {
url = "[" + url +"]";
}
MQTTMessage[] mqttMessages = new Gson().fromJson(url, MQTTMessage[].class);
Integer setCount = 1;
for(int i = 0; i < mqttMessages.length; i++) {
MQTTHandler mqttHandler = mqttHome.getMQTTHandler(mqttMessages[i].getClientId());
if(mqttHandler == null)
{
log.warn("Should not get here, no mqtt hanlder available");
responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + "\",\"description\": \"Should not get here, no mqtt handler available\", \"parameter\": \"/lights/" + lightId + "state\"}}]";
}
if(mqttMessages[i].getCount() != null && mqttMessages[i].getCount() > 0)
setCount = mqttMessages[i].getCount();
else
setCount = 1;
for(int x = 0; x < setCount; x++) {
if( x > 0) {
Thread.sleep(theDelay);
}
if(mqttMessages[i].getDelay() != null &&mqttMessages[i].getDelay() > 0)
theDelay = mqttMessages[i].getDelay();
log.debug("publishing message: " + mqttMessages[i].getClientId() + " - " + mqttMessages[i].getTopic() + " - " + mqttMessages[i].getMessage() + " - iteration: " + String.valueOf(i) + " - count: " + String.valueOf(x));
mqttHandler.publishMessage(mqttMessages[i].getTopic(), mqttMessages[i].getMessage());
}
}
}
else {
log.warn("Should not get here, no mqtt brokers configured");
responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + "\",\"description\": \"Should not get here, no mqtt brokers configured\", \"parameter\": \"/lights/" + lightId + "state\"}}]";
}
}
else if(device.getDeviceType().startsWith("exec")) {
log.debug("Exec Request called with url: " + url);
if(!url.startsWith("[")) {

View File

@@ -0,0 +1,30 @@
package com.bwssystems.mqtt;
import com.bwssystems.HABridge.NamedIP;
public class MQTTBroker {
private String clientId;
private String ip;
public MQTTBroker(NamedIP brokerConfig) {
super();
this.setIp(brokerConfig.getIp());
this.setClientId(brokerConfig.getName());
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
}

View File

@@ -51,6 +51,10 @@ public class MQTTHandler {
}
}
public NamedIP getMyConfig() {
return myConfig;
}
public void shutdown() {
try {
myClient.disconnect();

View File

@@ -1,7 +1,9 @@
package com.bwssystems.mqtt;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
@@ -53,4 +55,15 @@ public class MQTTHome {
return aHandler;
}
public List<MQTTBroker> getBrokers() {
Iterator<String> keys = handlers.keySet().iterator();
ArrayList<MQTTBroker> deviceList = new ArrayList<MQTTBroker>();
while(keys.hasNext()) {
String key = keys.next();
MQTTHandler aHandler = handlers.get(key);
MQTTBroker aDevice = new MQTTBroker(aHandler.getMyConfig());
deviceList.add(aDevice);
}
return deviceList;
}
}

View File

@@ -0,0 +1,39 @@
package com.bwssystems.mqtt;
public class MQTTMessage {
private String clientId;
private String topic;
private String message;
private Integer delay;
private Integer count;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getDelay() {
return delay;
}
public void setDelay(Integer delay) {
this.delay = delay;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
}

View File

@@ -37,6 +37,9 @@ app.config(function ($routeProvider) {
}).when('/haldevices', {
templateUrl: 'views/haldevice.html',
controller: 'HalController'
}).when('/mqttmessages', {
templateUrl: 'views/mqttpublish.html',
controller: 'MQTTController'
}).otherwise({
templateUrl: 'views/configuration.html',
controller: 'ViewingController'
@@ -62,7 +65,7 @@ String.prototype.replaceAll = function(search, replace)
app.service('bridgeService', function ($http, $window, ngToast) {
var self = this;
this.state = {base: window.location.origin + "/api/devices", bridgelocation: window.location.origin, systemsbase: window.location.origin + "/system", huebase: window.location.origin + "/api", configs: [], backups: [], devices: [], device: [], mapandid: [], type: "", settings: [], myToastMsg: [], logMsgs: [], loggerInfo: [], olddevicename: "", logShowAll: false, isInControl: false, showVera: false, showHarmony: false, showNest: false, showHue: false, showHal: false, habridgeversion: ""};
this.state = {base: window.location.origin + "/api/devices", bridgelocation: window.location.origin, systemsbase: window.location.origin + "/system", huebase: window.location.origin + "/api", configs: [], backups: [], devices: [], device: [], mapandid: [], type: "", settings: [], myToastMsg: [], logMsgs: [], loggerInfo: [], olddevicename: "", logShowAll: false, isInControl: false, showVera: false, showHarmony: false, showNest: false, showHue: false, showHal: false, showMqtt: false, habridgeversion: ""};
this.displayWarn = function(errorTitle, error) {
var toastContent = errorTitle;
@@ -189,6 +192,11 @@ app.service('bridgeService', function ($http, $window, ngToast) {
return;
}
this.updateShowMqtt = function () {
this.state.showMqtt = self.state.settings.mqttconfigured;
return;
}
this.loadBridgeSettings = function () {
return $http.get(this.state.systemsbase + "/settings").then(
function (response) {
@@ -198,6 +206,7 @@ app.service('bridgeService', function ($http, $window, ngToast) {
self.updateShowNest();
self.updateShowHue();
self.updateShowHal();
self.updateShowMqtt();
},
function (error) {
self.displayWarn("Load Bridge Settings Error: ", error);
@@ -340,6 +349,19 @@ app.service('bridgeService', function ($http, $window, ngToast) {
);
};
this.viewMQTTDevices = function () {
if(!this.state.showMqtt)
return;
return $http.get(this.state.base + "/mqtt/devices").then(
function (response) {
self.state.mqttbrokers = response.data;
},
function (error) {
self.displayWarn("Get MQTT Devices Error: ", error);
}
);
};
this.updateLogLevels = function(logComponents) {
return $http.put(this.state.systemsbase + "/logmgmt/update", logComponents ).then(
function (response) {
@@ -1728,6 +1750,71 @@ app.controller('HalController', function ($scope, $location, $http, bridgeServic
});
app.controller('MQTTController', function ($scope, $location, $http, bridgeService, ngDialog) {
$scope.bridge = bridgeService.state;
$scope.device = $scope.bridge.device;
bridgeService.viewMQTTDevices();
$scope.imgButtonsUrl = "glyphicon glyphicon-plus";
$scope.buttonsVisible = false;
$scope.clearDevice = function () {
bridgeService.clearDevice();
};
$scope.buildMQTTPublish = function (mqttbroker, mqtttopic, mqttmessage) {
var currentOn = $scope.device.onUrl;
var currentOff = $scope.device.offUrl;
if( $scope.device.mapType == "mqttMessage") {
$scope.device.mapId = $scope.device.mapId + "-" + mqtttopic;
$scope.device.onUrl = currentOn.substr(0, currentOn.indexOf("]")) + ",{\"clientId\":\"" + mqttbroker.clientId + "\",\"topic\":\"" + mqtttopic + "\",\"message\":\"" + mqttmessage + "\"}]";
$scope.device.offUrl = currentOff.substr(0, currentOff.indexOf("]")) + ",{\"clientId\":\"" + mqttbroker.clientId + "\",\"topic\":\"" + mqtttopic + "\",\"message\":\"" + mqttmessage + "\"}]";
}
else if ($scope.device.mapType == null || $scope.device.mapType == "") {
bridgeService.clearDevice();
$scope.device.deviceType = "mqtt";
$scope.device.targetDevice = mqttbroker.clientId;
$scope.device.name = mqttbroker.clientId + mqtttopic;
$scope.device.mapType = "mqttMessage";
$scope.device.mapId = mqttbroker.clientId + "-" + mqtttopic;
$scope.device.onUrl = "[{\"clientId\":\"" + mqttbroker.clientId + "\",\"topic\":\"" + mqtttopic + "\",\"message\":\"" + mqttmessage + "\"}]";
$scope.device.offUrl = "[{\"clientId\":\"" + mqttbroker.clientId + "\",\"topic\":\"" + mqtttopic + "\",\"message\":\"" + mqttmessage + "\"}]";
}
};
$scope.addDevice = function () {
if($scope.device.name == "" && $scope.device.onUrl == "")
return;
bridgeService.addDevice($scope.device).then(
function () {
$scope.clearDevice();
bridgeService.viewDevices();
bridgeService.viewMQTTDevices();
},
function (error) {
}
);
};
$scope.toggleButtons = function () {
$scope.buttonsVisible = !$scope.buttonsVisible;
if($scope.buttonsVisible)
$scope.imgButtonsUrl = "glyphicon glyphicon-minus";
else
$scope.imgButtonsUrl = "glyphicon glyphicon-plus";
};
$scope.deleteDeviceByMapId = function (id, mapType) {
$scope.bridge.mapandid = { id, mapType };
ngDialog.open({
template: 'deleteMapandIdDialog',
controller: 'DeleteMapandIdDialogCtrl',
className: 'ngdialog-theme-default'
});
};
});
app.controller('EditController', function ($scope, $location, $http, bridgeService) {
$scope.bridge = bridgeService.state;
$scope.device = $scope.bridge.device;
@@ -2002,6 +2089,20 @@ app.filter('configuredButtons', function() {
}
});
app.filter('configuredMqttMsgs', function() {
return function(input) {
var out = [];
if(input == null)
return out;
for (var i = 0; i < input.length; i++) {
if(input[i].mapType == "mqttMessage"){
out.push(input[i]);
}
}
return out;
}
});
app.controller('VersionController', function ($scope, bridgeService) {
$scope.bridge = bridgeService.state;
});

View File

@@ -17,6 +17,7 @@
href="#/huedevices">Hue Devices</a></li>
<li ng-if="bridge.showHal" role="presentation"><a
href="#/haldevices">HAL Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
</ul>

View File

@@ -13,9 +13,10 @@
<li ng-if="bridge.showNest" role="presentation"><a href="#/nest">Nest</a></li>
<li ng-if="bridge.showHue" role="presentation"><a
href="#/huedevices">Hue Devices</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
<li ng-if="bridge.showHal" role="presentation"><a
href="#/haldevices">HAL Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
<li role="presentation" class="active"><a href="#/editdevice">Edit
Device</a></li>
</ul>

View File

@@ -15,6 +15,7 @@
href="#/huedevices">Hue Devices</a></li>
<li ng-if="bridge.showHal" role="presentation"><a
href="#/haldevices">HAL Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation" class="active"><a href="#/editor">Manual
Add</a></li>
</ul>

View File

@@ -15,6 +15,7 @@
href="#/huedevices">Hue Devices</a></li>
<li role="presentation" class="active"><a href="#/haldevices">HAL
Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
</ul>

View File

@@ -15,6 +15,7 @@
href="#/huedevices">Hue Devices</a></li>
<li ng-if="bridge.showHal" role="presentation"><a
href="#/haldevices">HAL Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
</ul>

View File

@@ -15,6 +15,7 @@
href="#/huedevices">Hue Devices</a></li>
<li ng-if="bridge.showHal" role="presentation"><a
href="#/haldevices">HAL Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
</ul>

View File

@@ -15,6 +15,7 @@
Devices</a></li>
<li ng-if="bridge.showHal" role="presentation"><a
href="#/haldevices">HAL Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
</ul>

View File

@@ -15,6 +15,7 @@
href="#/huedevices">Hue Devices</a></li>
<li ng-if="bridge.showHal" role="presentation"><a
href="#/haldevices">HAL Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
</ul>

View File

@@ -28,7 +28,7 @@
setup for your MQTT Brokers.</p>
</div>
<scrollable-table watch="bridge.mqtt">
<scrollable-table watch="bridge.mqttbrokers">
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
@@ -40,21 +40,21 @@
<th>Actions</th>
</tr>
</thead>
<tr ng-repeat="harmonydevice in bridge.harmonydevices">
<tr ng-repeat="mqttbroker in bridge.mqttbrokers">
<td>{{$index+1}}</td>
<td>{{mqtt.broker.name}}</td>
<td>{{mqtt.broker.ip}}</td>
<td>{{mqttbroker.clientId}}</td>
<td>{{mqttbroker.ip}}</td>
<td>
<textarea rows="3" class="form-control" id="mqtt-topic"
ng-model="mqtttopic" placeholder="URL to turn device on"></textarea>
<textarea rows="2" class="form-control" id="mqtt-topic"
ng-model="mqtttopic" placeholder="The MQTT Topic"></textarea>
</td>
<td><
<textarea rows="3" class="form-control" id="mqtt-content"
ng-model="mqttcontent" placeholder="URL to turn device on"></textarea>
<td>
<textarea rows="2" class="form-control" id="mqtt-content"
ng-model="mqttcontent" placeholder="The MQTT Message Content"></textarea>
</td>
<td>
<button class="btn btn-success" type="submit"
ng-click="buildMQTTPublish(mqtt.broker, dmqtttopic, mqttcontent)">Build
ng-click="buildMQTTPublish(mqttbroker, mqtttopic, mqttcontent)">Build
publish Message</button>
</td>
</tr>
@@ -70,7 +70,7 @@
class={{imgButtonsUrl}} aria-hidden="true"></span></a>
</h2>
</div>
<scrollable-table ng-if="buttonsVisible" watch="bridge.mqtt">
<scrollable-table ng-if="buttonsVisible" watch="bridge.mqttbrokers">
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
@@ -82,7 +82,7 @@
</tr>
</thead>
<tr
ng-repeat="device in bridge.devices | configuredButtons | orderBy:predicate:reverse">
ng-repeat="device in bridge.devices | configuredMqttMsgs | orderBy:predicate:reverse">
<td>{{$index+1}}</td>
<td>{{device.name}}</td>
<td>{{device.targetDevice}}</td>

View File

@@ -15,6 +15,7 @@
href="#/huedevices">Hue Devices</a></li>
<li ng-if="bridge.showHal" role="presentation"><a
href="#/haldevices">HAL Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
</ul>

View File

@@ -16,6 +16,7 @@
href="#/huedevices">Hue Devices</a></li>
<li ng-if="bridge.showHal" role="presentation"><a
href="#/haldevices">HAL Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
</ul>

View File

@@ -14,6 +14,7 @@
href="#/huedevices">Hue Devices</a></li>
<li ng-if="bridge.showHal" role="presentation"><a
href="#/haldevices">HAL Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
</ul>

View File

@@ -14,6 +14,7 @@
href="#/huedevices">Hue Devices</a></li>
<li ng-if="bridge.showHal" role="presentation"><a
href="#/haldevices">HAL Devices</a></li>
<li ng-if="bridge.showMqtt" role="presentation"><a href="#/mqttmessages">MQTT Messages</a></li>
<li role="presentation"><a href="#/editor">Manual Add</a></li>
</ul>