作者 钟来

设备数据监听服务开发

... ... @@ -186,4 +186,9 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils
String time = System.currentTimeMillis() / 1000L + "";
return Integer.parseInt(time);
}
public static int getBeforeDawnTimeMilly(int now) {
int daySecond = 86400;
return now - (now + 28800) % daySecond;
}
}
... ...
... ... @@ -3,6 +3,7 @@ package com.zhonglai.luhui.alarm.dto;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.zhonglai.luhui.alarm.clas.UpAlarmFactory;
import java.lang.reflect.Method;
import java.util.List;
public class IotDevice {
... ...
... ... @@ -136,10 +136,6 @@ public class IotTerminal {
{
return null;
}
if(iotTerminal.getId().startsWith("864814074929612"))
{
System.out.println(iotTerminal.getUser_info_id());
}
if(null != iotTerminal.getUser_info_id())
{
CachAlarmConfig.addDeviceUser(iotTerminal.getId(),iotTerminal.getUser_info_id());
... ...
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zhonglai.luhui</groupId>
<artifactId>lh-modules</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>lh-deviceInfo-sync</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.zhonglai.luhui</groupId>
<artifactId>lh-service-dao</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
... ...
package com.luhui.deviceinfo.sync;
import com.luhui.deviceinfo.sync.service.CleanupTask;
import com.luhui.deviceinfo.sync.service.MySqlDeviceInfoLisenService;
public class DeviceInfoSyncMain {
public static void main(String[] args) {
// 注册Shutdown Hook
Runtime.getRuntime().addShutdownHook(new CleanupTask());
MySqlDeviceInfoLisenService.start();
}
}
... ...
package com.luhui.deviceinfo.sync.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CleanupTask extends Thread{
private static final Logger logger = LoggerFactory.getLogger(CleanupTask.class);
@Override
public void run() {
logger.info("程序关闭");
logger.info("关闭触发告警");
MySqlDeviceInfoLisenService.close();
}
}
... ...
package com.luhui.deviceinfo.sync.service;
import cn.hutool.db.nosql.redis.RedisDS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.Map;
public class JedisService {
private static final Logger logger = LoggerFactory.getLogger("redis-log");
private static final RedisDS deviceInfoRedis = RedisDS.create();
private static final RedisDS deviceHostRedis = RedisDS.create("devicehost");
private static void log(String action, Map<String, String> kvMap) {
StringBuilder sb = new StringBuilder();
sb.append("{\"tag\":\"jedis\",\"action\":\"").append(action).append("\"");
for (Map.Entry<String, String> entry : kvMap.entrySet()) {
sb.append(",\"").append(entry.getKey()).append("\":\"").append(entry.getValue()).append("\"");
}
sb.append("}");
logger.info(sb.toString());
}
public static Map<String, String> getDevicHost(String imei) {
Map<String, String> result = null;
try (Jedis jedis = deviceHostRedis.getJedis()) {
result = jedis.hgetAll(imei);
} catch (Exception e) {
Map<String, String> error = new HashMap<String, String>();
error.put("imei", imei);
error.put("error", e.getMessage());
log("getDevicHost_error", error);
}
return result;
}
public static Map<String, String> getDevicInfo(String deviceInfoId) {
Map<String, String> result = null;
try (Jedis jedis = deviceInfoRedis.getJedis()) {
result = jedis.hgetAll(deviceInfoId);
} catch (Exception e) {
Map<String, String> error = new HashMap<String, String>();
error.put("deviceInfoId", deviceInfoId);
error.put("error", e.getMessage());
log("getDevicInfo_error", error);
}
return result;
}
public static void setDevicInfo(String deviceInfoId, String attribute, String value) {
if (value == null) {
throw new IllegalArgumentException("Value cannot be null");
}
try (Jedis jedis = deviceInfoRedis.getJedis()) {
jedis.hset(deviceInfoId, attribute, value);
Map<String, String> info = new HashMap<String, String>();
info.put("deviceInfoId", deviceInfoId);
info.put("attribute", attribute);
info.put("value", value);
log("setDevicInfo", info);
} catch (Exception e) {
Map<String, String> error = new HashMap<String, String>();
error.put("deviceInfoId", deviceInfoId);
error.put("attribute", attribute);
error.put("value", value);
error.put("error", e.getMessage());
log("setDevicInfo_error", error);
}
}
public static void setDevicHost(String imei, String attribute, String value) {
if (value == null) {
throw new IllegalArgumentException("Value cannot be null");
}
try (Jedis jedis = deviceHostRedis.getJedis()) {
jedis.hset(imei, attribute, value);
Map<String, String> info = new HashMap<String, String>();
info.put("imei", imei);
info.put("attribute", attribute);
info.put("value", value);
log("setDevicHost", info);
} catch (Exception e) {
Map<String, String> error = new HashMap<String, String>();
error.put("imei", imei);
error.put("attribute", attribute);
error.put("value", value);
error.put("error", e.getMessage());
log("setDevicHost_error", error);
}
}
public static void setDevicHostBatch(String imei, Map<String, String> fields) {
if (fields == null || fields.containsValue(null)) {
throw new IllegalArgumentException("Fields cannot be null or contain null values");
}
try (Jedis jedis = deviceHostRedis.getJedis()) {
jedis.hmset(imei, fields);
Map<String, String> info = new HashMap<String, String>();
info.put("imei", imei);
info.put("fieldsCount", String.valueOf(fields.size()));
log("setDevicHostBatch", info);
} catch (Exception e) {
Map<String, String> error = new HashMap<String, String>();
error.put("imei", imei);
error.put("error", e.getMessage());
log("setDevicHostBatch_error", error);
}
}
public static void setDevicInfoBatchWithTTL(String imei, Map<String, String> fields, int ttlSeconds) {
if (fields == null ) {
throw new IllegalArgumentException("Fields cannot be null or contain null values");
}
if(fields.containsValue(null))
{
// 删除值为空的属性
fields.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue().isEmpty());
}
try (Jedis jedis = deviceInfoRedis.getJedis()) {
jedis.hmset(imei, fields);
if (ttlSeconds > 0) {
jedis.expire(imei, ttlSeconds);
}
// Map<String, String> info = new HashMap<String, String>();
// info.put("imei", imei);
// info.put("fieldsCount", String.valueOf(fields.size()));
// info.put("ttl", String.valueOf(ttlSeconds));
// log("setDevicHostBatchWithTTL", info);
} catch (Exception e) {
Map<String, String> error = new HashMap<String, String>();
error.put("imei", imei);
error.put("fieldsCount", String.valueOf(fields.size()));
error.put("ttl", String.valueOf(ttlSeconds));
error.put("error", e.getMessage());
log("setDevicHostBatchWithTTL_error", error);
}
}
public static void setDevicHostBatchWithTTL(String imei, Map<String, String> fields, int ttlSeconds) {
if (fields == null ) {
throw new IllegalArgumentException("Fields cannot be null or contain null values");
}
if(fields.containsValue(null))
{
// 删除值为空的属性
fields.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue().isEmpty());
}
try (Jedis jedis = deviceHostRedis.getJedis()) {
jedis.hmset(imei, fields);
if (ttlSeconds > 0) {
jedis.expire(imei, ttlSeconds);
}
// Map<String, String> info = new HashMap<String, String>();
// info.put("imei", imei);
// info.put("fieldsCount", String.valueOf(fields.size()));
// info.put("ttl", String.valueOf(ttlSeconds));
// log("setDevicHostBatchWithTTL", info);
} catch (Exception e) {
Map<String, String> error = new HashMap<String, String>();
error.put("imei", imei);
error.put("fieldsCount", String.valueOf(fields.size()));
error.put("ttl", String.valueOf(ttlSeconds));
error.put("error", e.getMessage());
log("setDevicHostBatchWithTTL_error", error);
}
}
public static void setDevicHostWithTTL(String imei, String attribute, String value, int ttlSeconds) {
if (value == null) {
throw new IllegalArgumentException("Value cannot be null");
}
try (Jedis jedis = deviceHostRedis.getJedis()) {
jedis.hset(imei, attribute, value);
jedis.expire(imei, ttlSeconds);
Map<String, String> info = new HashMap<String, String>();
info.put("imei", imei);
info.put("attribute", attribute);
info.put("value", value);
info.put("ttl", String.valueOf(ttlSeconds));
log("setDevicHostWithTTL", info);
} catch (Exception e) {
Map<String, String> error = new HashMap<String, String>();
error.put("imei", imei);
error.put("attribute", attribute);
error.put("value", value);
error.put("ttl", String.valueOf(ttlSeconds));
error.put("error", e.getMessage());
log("setDevicHostWithTTL_error", error);
}
}
public static boolean devicHostExists(String imei) {
boolean exists = false;
try (Jedis jedis = deviceHostRedis.getJedis()) {
exists = jedis.exists(imei);
Map<String, String> info = new HashMap<String, String>();
info.put("imei", imei);
info.put("exists", String.valueOf(exists));
log("devicHostExists", info);
} catch (Exception e) {
Map<String, String> error = new HashMap<String, String>();
error.put("imei", imei);
error.put("error", e.getMessage());
log("devicHostExists_error", error);
}
return exists;
}
public static void main(String[] args) {
Map<String, String> fields = new HashMap<String, String>();
fields.put("type", "MODEL-X");
fields.put("status", "online");
fields.put("alarm", "000");
setDevicHostBatch("test:123", fields);
}
}
... ...
package com.luhui.deviceinfo.sync.service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import com.luhui.deviceinfo.sync.util.ThreadPoolUtil;
import com.zhonglai.luhui.service.dao.BaseDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 监听mysql数据库的设备信息表
*/
public class MySqlDeviceInfoLisenService {
private static final Logger logger = LoggerFactory.getLogger(MySqlDeviceInfoLisenService.class);
private static BaseDao baseDao = new BaseDao();
private static ScheduledFuture scheduledDeviceHost;
private static ScheduledFuture scheduledDeviceInfo;
private static ScheduledFuture scheduledWdbTerminal;
private static ScheduledFuture scheduledIotTerminal;
private static ScheduledFuture scheduledIotDevice;
public static void start() {
// 异步获取数据
scheduledDeviceHost = ThreadPoolUtil.executor.scheduleWithFixedDelay(() -> {
try {
upDeviceHost();
}catch (Exception e)
{
logger.info("触发告警业务异常",e);
}
},0,30, TimeUnit.SECONDS);
scheduledDeviceInfo = ThreadPoolUtil.executor.scheduleWithFixedDelay(() -> {
try {
upDeviceInfo();
}catch (Exception e)
{
logger.info("触发告警业务异常",e);
}
},0,30, TimeUnit.SECONDS);
scheduledWdbTerminal = ThreadPoolUtil.executor.scheduleWithFixedDelay(() -> {
try {
upWdbTerminal();
}catch (Exception e)
{
logger.info("触发告警业务异常",e);
}
},0,30, TimeUnit.SECONDS);
scheduledIotTerminal = ThreadPoolUtil.executor.scheduleWithFixedDelay(() -> {
try {
upIotTerminal();
}catch (Exception e)
{
logger.info("触发告警业务异常",e);
}
},0,30, TimeUnit.SECONDS);
scheduledIotDevice = ThreadPoolUtil.executor.scheduleWithFixedDelay(() -> {
try {
upIotDevice();
}catch (Exception e)
{
logger.info("触发告警业务异常",e);
}
},0,30, TimeUnit.SECONDS);
}
public static void close() {
// 取消任务并关闭资源
cancelTask(scheduledDeviceHost);
cancelTask(scheduledDeviceInfo);
cancelTask(scheduledWdbTerminal);
cancelTask(scheduledIotTerminal);
cancelTask(scheduledIotDevice);
ThreadPoolUtil.close();
}
private static void cancelTask(ScheduledFuture<?> task) {
if (task != null) {
task.cancel(false);
}
}
private static void upDeviceHost()
{
try {
List<Map<String,Object>> mapList = baseDao.findListBysql("SELECT id,device_model,`data`,data_update_time,alarm_code,interval_time FROM liu_yu_le.device_host WHERE data_update_time >= UNIX_TIMESTAMP(NOW()) - 60");
if(null != mapList && mapList.size()>0)
{
long time = System.currentTimeMillis();
for (Map<String,Object> map : mapList)
{
String id = (String) map.get("id");
map.remove("id");
Integer interval_time = (Integer) map.get("interval_time");
if (null == interval_time)
{
interval_time = 60;
}
Map<String,String> savemap = new HashMap<>();
savemap.put("type", (String) map.get("device_model"));
savemap.put("dataState", (String) map.get("data_state"));
savemap.put("dataUpdateTime", map.get("data_update_time")+"");
savemap.put("deviceConfig", (String) map.get("device_config"));
savemap.put("alarm", (String) map.get("alarm_code"));
JedisService.setDevicHostBatchWithTTL(id, savemap, interval_time);
}
logger.info("鱼儿乐主机更新条数:{},耗时 {} ms",mapList.size(),time-System.currentTimeMillis());
}
}catch (Exception e)
{
logger.info("数据处理异常",e);
}
}
private static void upDeviceInfo()
{
try {
List<Map<String,Object>> mapList = baseDao.findBysql("SELECT a.id,a.device_model,a.data_state,a.data_update_time,a.device_config,a.alarm_code,b.`interval_time` FROM liu_yu_le.device_info a LEFT JOIN liu_yu_le.`device_host` b ON a.`device_id`=b.`id` WHERE a.data_update_time >= UNIX_TIMESTAMP(NOW()) - 60");
if(null != mapList && mapList.size()>0)
{
long time = System.currentTimeMillis();
for (Map<String,Object> map : mapList)
{
String id = (String) map.get("id");
Map<String,String> savemap = new HashMap<>();
savemap.put("type", (String) map.get("device_model"));
savemap.put("dataState", (String) map.get("data_state"));
savemap.put("dataUpdateTime", map.get("data_update_time")+"");
savemap.put("deviceConfig", (String) map.get("device_config"));
savemap.put("alarm", (String) map.get("alarm_code"));
Integer interval_time = (Integer) map.get("interval_time");
if (null == interval_time)
{
interval_time = 60;
}
JedisService.setDevicInfoBatchWithTTL(id, savemap, interval_time);
}
logger.info("设备表更新条数:{},耗时 {} ms",mapList.size(),time-System.currentTimeMillis());
}
}catch (Exception e)
{
logger.info("数据处理异常",e);
}
}
private static void upWdbTerminal()
{
try {
List<Map<String,Object>> mapList = baseDao.findBysql("SELECT a.id,a.`data`,a.data_update_time,a.alarm_code,b.`interval_time` FROM liu_yu_le.wdb_terminal a LEFT JOIN liu_yu_le.`device_host` b ON a.`base_station_id`=b.`id` WHERE a.data_update_time >= UNIX_TIMESTAMP(NOW()) - 60");
if(null != mapList && mapList.size()>0)
{
long time = System.currentTimeMillis();
for (Map<String,Object> map : mapList)
{
String id = (String) map.get("id");
Map<String,String> savemap = new HashMap<>();
savemap.put("type", "WDB");
savemap.put("dataState", (String) map.get("data"));
savemap.put("dataUpdateTime", map.get("data_update_time")+"");
savemap.put("alarm", (String) map.get("alarm_code"));
Integer interval_time = (Integer) map.get("interval_time");
if (null == interval_time)
{
interval_time = 60;
}
JedisService.setDevicInfoBatchWithTTL(id, savemap, interval_time);
}
logger.info("温度宝表更新条数:{},耗时 {} ms",mapList.size(),time-System.currentTimeMillis());
}
}catch (Exception e)
{
logger.info("数据处理异常",e);
}
}
private static void upIotTerminal()
{
try {
List<Map<String,Object>> mapList = baseDao.findBysql("SELECT a.id,a.`mqtt_username`,a.`things_model_value`,a.`things_model_config`,a.`update_time`,b.device_life FROM `mqtt_broker`.`iot_terminal` a LEFT JOIN `mqtt_broker`.`iot_device` b ON a.`device_id`=b.`client_id` WHERE a.data_update_time >= UNIX_TIMESTAMP(NOW()) - 60");
if(null != mapList && mapList.size()>0)
{
long time = System.currentTimeMillis();
for (Map<String,Object> map : mapList)
{
String id = (String) map.get("id");
Map<String,String> savemap = new HashMap<>();
savemap.put("type", (String) map.get("mqtt_username"));
savemap.put("things_model_value", (String) map.get("things_model_value"));
savemap.put("things_model_config", (String) map.get("things_model_config"));
savemap.put("dataUpdateTime", null != map.get("update_time")? map.get("update_time")+"":null);
Integer interval_time = null != map.get("device_life")? Integer.parseInt(map.get("device_life")+""):60;
JedisService.setDevicInfoBatchWithTTL(id, savemap, interval_time);
}
logger.info("终端表更新条数:{},耗时 {} ms",mapList.size(),time-System.currentTimeMillis());
}
}catch (Exception e)
{
logger.info("数据处理异常",e);
}
}
private static void upIotDevice()
{
try {
List<Map<String,Object>> mapList = baseDao.findBysql("SELECT `client_id`,`mqtt_username`,`things_model_value`,`things_model_config`,`update_time`,device_life FROM `mqtt_broker`.`iot_device` WHERE data_update_time >= UNIX_TIMESTAMP(NOW()) - 60");
if(null != mapList && mapList.size()>0)
{
long time = System.currentTimeMillis();
for (Map<String,Object> map : mapList)
{
String id = (String) map.get("client_id");
Map<String,String> savemap = new HashMap<>();
savemap.put("type", (String) map.get("mqtt_username"));
savemap.put("things_model_value", (String) map.get("things_model_value"));
savemap.put("things_model_config", map.get("things_model_config")+"");
savemap.put("dataUpdateTime", null != map.get("update_time")? map.get("update_time")+"":null);
Integer interval_time = null != map.get("device_life")? Integer.parseInt(map.get("device_life")+""):60;
JedisService.setDevicHostBatchWithTTL(id, savemap, interval_time);
}
logger.info("主机表更新条数:{},耗时 {} ms",mapList.size(),time-System.currentTimeMillis());
}
}catch (Exception e)
{
logger.info("数据处理异常",e);
}
}
}
... ...
package com.luhui.deviceinfo.sync.util;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolUtil {
private static int poolSize = 10; // 线程池大小
public static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor (poolSize, Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
public static void close()
{
// 关闭调度器
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
... ...
#-------------------------------------------------------------------------------
# Redis客户端配置样例
# 每一个分组代表一个Redis实例
# 无分组的Pool配置为所有分组的共用配置,如果分组自己定义Pool配置,则覆盖共用配置
# 池配置来自于:https://www.cnblogs.com/jklk/p/7095067.html
#-------------------------------------------------------------------------------
#----- 默认(公有)配置
# 地址,默认localhost
host = 119.23.218.181
# 端口,默认6379
port = 6379
# 超时,默认2000
timeout = 2000
# 连接超时,默认timeout
connectionTimeout = 2000
# 读取超时,默认timeout
soTimeout = 2000
# 密码,默认无
# password =
# 数据库序号,默认0
database = 2
# 客户端名,默认"Hutool"
clientName = Hutool
# SSL连接,默认false
ssl = false;
#----- 自定义分组的连接
[devicehost]
# 地址,默认localhost
host = 119.23.218.181
port = 6379
# password =
database = 3
# 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true
BlockWhenExhausted = true;
# 设置的逐出策略类名, 默认DefaultEvictionPolicy(当连接超过最大空闲时间,或连接数超过最大空闲连接数)
evictionPolicyClassName = org.apache.commons.pool2.impl.DefaultEvictionPolicy
# 是否启用pool的jmx管理功能, 默认true
jmxEnabled = true;
# 是否启用后进先出, 默认true
lifo = true;
# 最大空闲连接数, 默认8个
maxIdle = 8
# 最小空闲连接数, 默认0
minIdle = 0
# 最大连接数, 默认8个
maxTotal = 8
# 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1
maxWaitMillis = -1
# 逐出连接的最小空闲时间 默认1800000毫秒(30分钟)
minEvictableIdleTimeMillis = 1800000
# 每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3
numTestsPerEvictionRun = 3;
# 对象空闲多久后逐出, 当空闲时间>该值 且 空闲连接>最大空闲数 时直接逐出,不再根据MinEvictableIdleTimeMillis判断 (默认逐出策略)
SoftMinEvictableIdleTimeMillis = 1800000
# 在获取连接的时候检查有效性, 默认false
testOnBorrow = false
# 在空闲时检查有效性, 默认false
testWhileIdle = false
# 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
timeBetweenEvictionRunsMillis = -1
\ No newline at end of file
... ...
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/output.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/output.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>5</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="FILE" />
<appender-ref ref="CONSOLE" />
</root>
</configuration>
\ No newline at end of file
... ...
... ... @@ -36,6 +36,7 @@
<module>lh-superweb</module>
<module>lh-superweb-jar</module>
<module>lh-ssh-service-lesten</module>
<module>lh-deviceInfo-sync</module>
</modules>
<properties>
... ...