1. IOT创建新产品 睡眠检测带
2. 养老后台 添加了一个设备 睡眠检测带_201_1
3. 新增了模拟器(3个模拟js运行)
4. 创建了消费者组(默认DEFAULT)
5. 创建订阅(3个产品的上报信息 传给DEFAULT)
消息处理
前面我们已经完成了设备的管理,现在,我们就来处理设备联网之后上报数据的处理
从图中我们可以看到,当设备上报数据到IOT平台之后,业务系统可以从IOT平台中拉取数据到自己的数据库,方便在业务应用中去展示。
在IOT中数据流转是这样的,如下图
在IOT官方文档中,已经提供了对应的接收数据的解决方案,如下链接:
https://help.aliyun.com/zh/iot/developer-reference/connect-a-client-to-iot-platform-by-using-the-sdk-for-java?spm=a2c4g.11186623.0.0.7d7234bdQCi7MQ
官网SDK接入
导入依赖
<!-- amqp 1.0 qpid client -->
<dependency>
<groupId>org.apache.qpid</groupId>
<artifactId>qpid-jms-client</artifactId>
<version>0.57.0</version>
</dependency>
<!-- util for base64-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
下载Demo
下载地址:https://linkkit-export.oss-cn-shanghai.aliyuncs.com/amqp/amqp-demo.zip
接收数据
我们可以修改里面的参数,包含以下几个重要参数:
-
accessKey 秘钥key
-
accessSecret 秘钥
-
iotInstanceId 公共实例ID
-
clientId:InetAddress.getLocalHost().getHostAddress(); 获取本机ip作为clientId
-
consumerGroupId 消费者组
修改之后的代码:
package com.aliyun.iotx.demo;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.naming.Context;
import javax.naming.InitialContext;
import org.apache.commons.codec.binary.Base64;
import org.apache.qpid.jms.JmsConnection;
import org.apache.qpid.jms.JmsConnectionListener;
import org.apache.qpid.jms.message.JmsInboundMessageDispatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AmqpClient {
private final static Logger logger = LoggerFactory.getLogger(AmqpClient.class);
/**
* 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例使用环境变量获取 AccessKey 的方式进行调用,仅供参考
*/
private static String accessKey = "LTAI5tDQKg9F61aJhbmhqVRK";
private static String accessSecret = "LYUKZH7HQGBoD025pmSq0fQsREaOYD";;
private static String consumerGroupId = "eraicKJm98cQR0hHgsxb000100";
//iotInstanceId:实例ID。若是2021年07月30日之前(不含当日)开通的公共实例,请填空字符串。
private static String iotInstanceId = "iot-06z00frq8umvkx2";
//控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。
//建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。
private static String clientId;
static {
try {
clientId = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
//${YourHost}为接入域名,请参见AMQP客户端接入说明文档。
private static String host = "iot-06z00frq8umvkx2.amqp.iothub.aliyuncs.com";
// 指定单个进程启动的连接数
// 单个连接消费速率有限,请参考使用限制,最大64个连接
// 连接数和消费速率及rebalance相关,建议每500QPS增加一个连接
private static int connectionCount = 4;
//业务处理异步线程池,线程池参数可以根据您的业务特点调整,或者您也可以用其他异步方式处理接收到的消息。
private final static ExecutorService executorService = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors() * 2, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue(50000));
public static void main(String[] args) throws Exception {
List<Connection> connections = new ArrayList<>();
//参数说明,请参见AMQP客户端接入说明文档。
for (int i = 0; i < connectionCount; i++) {
long timeStamp = System.currentTimeMillis();
//签名方法:支持hmacmd5、hmacsha1和hmacsha256。
String signMethod = "hmacsha1";
//userName组装方法,请参见AMQP客户端接入说明文档。
String userName = clientId + "-" + i + "|authMode=aksign"
+ ",signMethod=" + signMethod
+ ",timestamp=" + timeStamp
+ ",authId=" + accessKey
+ ",iotInstanceId=" + iotInstanceId
+ ",consumerGroupId=" + consumerGroupId
+ "|";
//计算签名,password组装方法,请参见AMQP客户端接入说明文档。
String signContent = "authId=" + accessKey + "×tamp=" + timeStamp;
String password = doSign(signContent, accessSecret, signMethod);
String connectionUrl = "failover:(amqps://" + host + ":5671?amqp.idleTimeout=80000)"
+ "?failover.reconnectDelay=30";
Hashtable<String, String> hashtable = new Hashtable<>();
hashtable.put("connectionfactory.SBCF", connectionUrl);
hashtable.put("queue.QUEUE", "default");
hashtable.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.qpid.jms.jndi.JmsInitialContextFactory");
Context context = new InitialContext(hashtable);
ConnectionFactory cf = (ConnectionFactory)context.lookup("SBCF");
Destination queue = (Destination)context.lookup("QUEUE");
// 创建连接。
Connection connection = cf.createConnection(userName, password);
connections.add(connection);
((JmsConnection)connection).addConnectionListener(myJmsConnectionListener);
// 创建会话。
// Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。
// Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
connection.start();
// 创建Receiver连接。
MessageConsumer consumer = session.createConsumer(queue);
consumer.setMessageListener(messageListener);
}
logger.info("amqp demo is started successfully, and will exit after 60s ");
// 结束程序运行
Thread.sleep(6000 * 1000);
logger.info("run shutdown");
connections.forEach(c-> {
try {
c.close();
} catch (JMSException e) {
logger.error("failed to close connection", e);
}
});
executorService.shutdown();
if (executorService.awaitTermination(10, TimeUnit.SECONDS)) {
logger.info("shutdown success");
} else {
logger.info("failed to handle messages");
}
}
private static MessageListener messageListener = new MessageListener() {
@Override
public void onMessage(final Message message) {
try {
//1.收到消息之后一定要ACK。
// 推荐做法:创建Session选择Session.AUTO_ACKNOWLEDGE,这里会自动ACK。
// 其他做法:创建Session选择Session.CLIENT_ACKNOWLEDGE,这里一定要调message.acknowledge()来ACK。
// message.acknowledge();
//2.建议异步处理收到的消息,确保onMessage函数里没有耗时逻辑。
// 如果业务处理耗时过程过长阻塞住线程,可能会影响SDK收到消息后的正常回调。
executorService.submit(new Runnable() {
@Override
public void run() {
processMessage(message);
}
});
} catch (Exception e) {
logger.error("submit task occurs exception ", e);
}
}
};
/**
* 在这里处理您收到消息后的具体业务逻辑。
*/
private static void processMessage(Message message) {
try {
byte[] body = message.getBody(byte[].class);
String content = new String(body);
String topic = message.getStringProperty("topic");
String messageId = message.getStringProperty("messageId");
logger.info("receive message"
+ ",\n topic = " + topic
+ ",\n messageId = " + messageId
+ ",\n content = " + content);
} catch (Exception e) {
logger.error("processMessage occurs error ", e);
}
}
private static JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() {
/**
* 连接成功建立。
*/
@Override
public void onConnectionEstablished(URI remoteURI) {
logger.info("onConnectionEstablished, remoteUri:{}", remoteURI);
}
/**
* 尝试过最大重试次数之后,最终连接失败。
*/
@Override
public void onConnectionFailure(Throwable error) {
logger.error("onConnectionFailure, {}", error.getMessage());
}
/**
* 连接中断。
*/
@Override
public void onConnectionInterrupted(URI remoteURI) {
logger.info("onConnectionInterrupted, remoteUri:{}", remoteURI);
}
/**
* 连接中断后又自动重连上。
*/
@Override
public void onConnectionRestored(URI remoteURI) {
logger.info("onConnectionRestored, remoteUri:{}", remoteURI);
}
@Override
public void onInboundMessage(JmsInboundMessageDispatch envelope) {}
@Override
public void onSessionClosed(Session session, Throwable cause) {}
@Override
public void onConsumerClosed(MessageConsumer consumer, Throwable cause) {}
@Override
public void onProducerClosed(MessageProducer producer, Throwable cause) {}
};
/**
* 计算签名,password组装方法,请参见AMQP客户端接入说明文档。
*/
private static String doSign(String toSignString, String secret, String signMethod) throws Exception {
SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), signMethod);
Mac mac = Mac.getInstance(signMethod);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(toSignString.getBytes());
return Base64.encodeBase64String(rawHmac);
}
}
以上代码启动之后,并不能接收到数据,因为设备并没有绑定topic,所以需要在物联网IOT平台设置topic,也就是消费者的消费者组
第一:找到 消息转发->服务端订阅->消费者组列表
创建一个自己的消费者组
创建好之后可以查看到已经创建好的消费者组,并且自动生成了消费者组id
进入刚刚创建的消费者组,然后点击订阅产品,然后创建订阅
需要选择消费者组与推送消息类型(设备上报数据),如下图
修改demo代码中的消费者组,改为自己创建的消费者组ID
private static String consumerGroupId = "eraicKJm98cQR0hHgsxb000100";
测试
找一个设备进行数据上报
demo代码中绑定对应的消费者组
启动后台代码,可以在日志中查看消费者到的数据
接收设备端数据
SDK提供好的这个工具类,我们需要改造这个类,改造内容如下:
- 让spring进行管理和监听,一旦有数据变化之后,就可以马上消费,可以让这个类实现ApplicationRunner接口,重新run方法
- 可以在项目中自己配置线程池的使用
- 所有的可变参数,如实例id、accessKey、accessSecret、consumerGroupId这些统一在配置文件中维护
改造AmqpClient
package com.zzyl.listener;
import com.zzyl.properties.AliIoTConfigProperties;
import org.apache.commons.codec.binary.Base64;
import org.apache.qpid.jms.JmsConnection;
import org.apache.qpid.jms.JmsConnectionListener;
import org.apache.qpid.jms.message.JmsInboundMessageDispatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.jms.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ExecutorService;
@Component
public class AmqpClient implements ApplicationRunner {
private final static Logger logger = LoggerFactory.getLogger(AmqpClient.class);
@Autowired
private AliIoTConfigProperties aliIoTConfigProperties;
//控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。
//建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。
private static String clientId;
static {
try {
clientId = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
// 指定单个进程启动的连接数
// 单个连接消费速率有限,请参考使用限制,最大64个连接
// 连接数和消费速率及rebalance相关,建议每500QPS增加一个连接
private static int connectionCount = 64;
//业务处理异步线程池,线程池参数可以根据您的业务特点调整,或者您也可以用其他异步方式处理接收到的消息。
@Autowired
private ExecutorService executorService;
public void start() throws Exception {
List<Connection> connections = new ArrayList<>();
//参数说明,请参见AMQP客户端接入说明文档。
for (int i = 0; i < connectionCount; i++) {
long timeStamp = System.currentTimeMillis();
//签名方法:支持hmacmd5、hmacsha1和hmacsha256。
String signMethod = "hmacsha1";
//userName组装方法,请参见AMQP客户端接入说明文档。
String userName = clientId + "-" + i + "|authMode=aksign"
+ ",signMethod=" + signMethod
+ ",timestamp=" + timeStamp
+ ",authId=" + aliIoTConfigProperties.getAccessKeyId()
+ ",iotInstanceId=" + aliIoTConfigProperties.getIotInstanceId()
+ ",consumerGroupId=" + aliIoTConfigProperties.getConsumerGroupId()
+ "|";
//计算签名,password组装方法,请参见AMQP客户端接入说明文档。
String signContent = "authId=" + aliIoTConfigProperties.getAccessKeyId() + "×tamp=" + timeStamp;
String password = doSign(signContent, aliIoTConfigProperties.getAccessKeySecret(), signMethod);
String connectionUrl = "failover:(amqps://" + aliIoTConfigProperties.getHost() + ":5671?amqp.idleTimeout=80000)"
+ "?failover.reconnectDelay=30";
Hashtable<String, String> hashtable = new Hashtable<>();
hashtable.put("connectionfactory.SBCF", connectionUrl);
hashtable.put("queue.QUEUE", "default");
hashtable.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.qpid.jms.jndi.JmsInitialContextFactory");
Context context = new InitialContext(hashtable);
ConnectionFactory cf = (ConnectionFactory) context.lookup("SBCF");
Destination queue = (Destination) context.lookup("QUEUE");
// 创建连接。
Connection connection = cf.createConnection(userName, password);
connections.add(connection);
((JmsConnection) connection).addConnectionListener(myJmsConnectionListener);
// 创建会话。
// Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。
// Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
connection.start();
// 创建Receiver连接。
MessageConsumer consumer = session.createConsumer(queue);
consumer.setMessageListener(messageListener);
}
logger.info("amqp is started successfully, and will exit after server shutdown ");
}
private MessageListener messageListener = message -> {
try {
//异步处理收到的消息,确保onMessage函数里没有耗时逻辑
executorService.submit(() -> processMessage(message));
} catch (Exception e) {
logger.error("submit task occurs exception ", e);
}
};
/**
* 在这里处理您收到消息后的具体业务逻辑。
*/
private void processMessage(Message message) {
try {
byte[] body = message.getBody(byte[].class);
String contentStr = new String(body);
String topic = message.getStringProperty("topic");
String messageId = message.getStringProperty("messageId");
logger.info("receive message"
+ ",\n topic = " + topic
+ ",\n messageId = " + messageId
+ ",\n content = " + contentStr);
} catch (Exception e) {
logger.error("processMessage occurs error ", e);
}
}
private JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() {
/**
* 连接成功建立。
*/
@Override
public void onConnectionEstablished(URI remoteURI) {
logger.info("onConnectionEstablished, remoteUri:{}", remoteURI);
}
/**
* 尝试过最大重试次数之后,最终连接失败。
*/
@Override
public void onConnectionFailure(Throwable error) {
logger.error("onConnectionFailure, {}", error.getMessage());
}
/**
* 连接中断。
*/
@Override
public void onConnectionInterrupted(URI remoteURI) {
logger.info("onConnectionInterrupted, remoteUri:{}", remoteURI);
}
/**
* 连接中断后又自动重连上。
*/
@Override
public void onConnectionRestored(URI remoteURI) {
logger.info("onConnectionRestored, remoteUri:{}", remoteURI);
}
@Override
public void onInboundMessage(JmsInboundMessageDispatch envelope) {
}
@Override
public void onSessionClosed(Session session, Throwable cause) {
}
@Override
public void onConsumerClosed(MessageConsumer consumer, Throwable cause) {
}
@Override
public void onProducerClosed(MessageProducer producer, Throwable cause) {
}
};
/**
* 计算签名,password组装方法,请参见AMQP客户端接入说明文档。
*/
private static String doSign(String toSignString, String secret, String signMethod) throws Exception {
SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), signMethod);
Mac mac = Mac.getInstance(signMethod);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(toSignString.getBytes());
return Base64.encodeBase64String(rawHmac);
}
@Override
public void run(ApplicationArguments args) throws Exception {
start();
}
}
设备消息订阅
在接收消息之前,我们需要让设备绑定消费组列表,这样才能通过消费组去接收消息
第一:找到 消息转发->服务端订阅->消费者组列表
目前有一个默认的消费组
第二:创建订阅,让产品与消费组进行关联
在服务端订阅页面的订阅列表页签下,单击创建订阅。
在创建订阅对话框,设置参数后单击确认。
参数 | 说明 |
---|---|
产品 | 选择自己的产品 |
订阅类型 | 选择AMQP |
消费组 | 选择默认消费组 |
推送消息类型 | 选择设备上报消息 |
保存设备端数据
思路分析
功能实现
接收到的数据格式
{
"deviceType":"ActiveInfraredIntrusionDetectors",
"iotId":"DoXPJsxUkV0Kcw9zrLRaj0rk00",
"requestId":"1699948275310",
"checkFailedData":{
},
"productKey":"j0rkHpZoAQ3",
"gmtCreate":1699948275451,
"deviceName":"yangan_09",
"items":{
"CurrentHumidity":{
"value":75,
"time":1699948275447
},
"BatteryLevel":{
"value":75,
"time":1699948275447
},
"SmokeSensorState":{
"value":0,
"time":1699948275447
},
"IndoorTemperature":{
"value":27,
"time":1699948275447
}
}
}
修改AmqpClient类中的processMessage,方法
@Autowired
private DeviceMapper deviceMapper;
@Autowired
private DeviceDataService deviceDataService;
/**
* 在这里处理您收到消息后的具体业务逻辑。
*/
private void processMessage(Message message) {
try {
byte[] body = message.getBody(byte[].class);
String contentStr = new String(body);
String topic = message.getStringProperty("topic");
String messageId = message.getStringProperty("messageId");
logger.info("receive message"
+ ",\n topic = " + topic
+ ",\n messageId = " + messageId
+ ",\n content = " + contentStr);
//解析数据
Content content = JSONUtil.toBean(contentStr, Content.class);
//查询设备是否存在
LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Device::getIotId,content.getIotId());
Device device = deviceMapper.selectOne(wrapper);
if (device == null){
logger.error(content.getIotId()+"设备不存在");
return;//结束本次调用
}
//从content中获取设备数据,保存到monitor_device_data表
List<DeviceData> list = new ArrayList<>();
content.getItems().forEach((k,v)->{
DeviceData deviceData = new DeviceData();
deviceData.setDeviceName(device.getDeviceName());//设备名称
deviceData.setIotId(content.getIotId());//iotId
deviceData.setNickname(device.getNickname());//备注名称
deviceData.setProductKey(device.getProductKey());//设备key
deviceData.setProductName(device.getProductName());//设备名称
deviceData.setFunctionId(k);//功能名称
deviceData.setAccessLocation(device.getRemark());//接入位置
deviceData.setLocationType(device.getLocationType());//位置类型
deviceData.setPhysicalLocationType(device.getPhysicalLocationType());//物理位置类型
deviceData.setDeviceDescription(device.getDeviceDescription());//设备描述
deviceData.setAlarmTime(LocalDateTimeUtil.of(v.getTime()));//数据上报时间
deviceData.setDataValue(v.getValue()+"");//数据值
list.add(deviceData);
});
//批量保存
deviceDataService.saveBatch(list);
} catch (Exception e) {
logger.error("processMessage occurs error ", e);
}
}
测试
(1)启动模拟设备进行上报
(2)启动后端项目,查看数据库是否保存数据成功
查看设备的物模型数据
需求分析
我们先打开原型图,如下:
其中的数据值,是从IOT平台实时获取到的数据值
当点击了某个功能(物模型)的查看数据按钮,则会显示这个功能的历史数据,可以按照时间范围进行检索,如下图:
时间范围=1小时、24小时、7天、自定义;当选择自定义时,出时间选择器文本框
目前需要两个接口,都是查询
- 查询物模型列表及其数据:实时查询IOT平台,昨天已经实现,我们现在只需要实现第二个即可
- 查询单个物模型的历史数据
DeviceDataController
package com.zzyl.controller.web;
import com.zzyl.base.PageResponse;
import com.zzyl.base.ResponseResult;
import com.zzyl.controller.BaseController;
import com.zzyl.dto.DeviceDataPageReqDto;
import com.zzyl.entity.DeviceData;
import com.zzyl.service.DeviceDataService;
import com.zzyl.vo.DeviceDataVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
//设备数据
@RestController
public class DeviceDataController extends BaseController {
@Autowired
private DeviceDataService deviceDataService;
//获取设备数据分页结果
@GetMapping("/device-data/get-page")
public ResponseResult getDeviceDataPage(DeviceDataPageReqDto deviceDataPageReqDto){
PageResponse<DeviceData> pageResponse = deviceDataService.findByPage(deviceDataPageReqDto);
return success(pageResponse);
}
}
DeviceDataService
//条件分页查询设备数据
PageResponse<DeviceData> findByPage(DeviceDataPageReqDto deviceDataPageReqDto);
DeviceDataServiceImpl
@Override
public PageResponse<DeviceData> findByPage(DeviceDataPageReqDto dto) {
//1. 分页参数
Page<DeviceData> page = new Page<>(dto.getPageNum(), dto.getPageSize());
//2. 业务参数
LambdaQueryWrapper<DeviceData> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StrUtil.isNotEmpty(dto.getDeviceName()), DeviceData::getDeviceName, dto.getDeviceName())
.eq(StrUtil.isNotEmpty(dto.getFunctionId()), DeviceData::getFunctionId, dto.getFunctionId())
.between(dto.getStartDate() != null && dto.getEndDate() != null, DeviceData::getAlarmTime, dto.getStartDate(), dto.getEndDate());
//3. 执行查询
page = getBaseMapper().selectPage(page, wrapper);
return new PageResponse(page);
}
测试
打开设备详情,找到物模型数据,点击查看数据按钮,查询数据,是否正常
智能床位
需求分析
在床位管理里面有个智能床位,可以展示绑定智能设备的房间或床位以及设备对应的数据
其中数据有每分钟动态展示一次,如果某个设备的数据异常,则会在tab选项卡中进行提示
![](assets/智能床位-原型.png)
获取所有带智能设备的楼层
查询包含智能设备的楼层(房间和床位只有有一项有智能设备就要查)
-- 查询出所有带有智能设备的楼层(楼层中至少一间房有智能设备 或者 楼层中至少某个床绑定了智能设备)
select bf.code,br.code,md.device_name,bb.bed_number,md2.device_name from base_floor bf
left join base_room br on br.floor_id = bf.id
left join monitor_device md on md.physical_location_type = 1 and md.binding_location = br.id
left join base_bed bb on bb.room_id = br.id
left join monitor_device md2 on md2.physical_location_type = 2 and md2.binding_location = bb.id
where (md.id is not null or md2.id is not null)
group by bf.code
order by bf.code
-- 如果楼层的智能设备参与
select bf.code,br.code,md.device_name,bb.bed_number,md2.device_name from base_floor bf
left join base_room br on br.floor_id = bf.id
left join monitor_device md on md.physical_location_type = 1 and md.binding_location = br.id
left join base_bed bb on bb.room_id = br.id
left join monitor_device md2 on md2.physical_location_type = 2 and md2.binding_location = bb.id
left join monitor_device md3 on md3.physical_location_type = 0 and md2.binding_location = bf.id
where (md.id is not null or md2.id is not null or md3.id is not null)
group by bf.code
order by bf.code
FloorController
//获取所有楼层 (智能楼层)
@GetMapping("/floor/getAllFloorsWithDevice")
public ResponseResult getAllFloorsWithDevice(){
List<Floor> list = floorService.getAllFloorsWithDevice();
return success(list);
}
FloorService
//查询包含智能设备的楼层
List<Floor> getAllFloorsWithDevice();
FloorServiceImpl
//查询包含智能设备的楼层
@Override
public List<Floor> getAllFloorsWithDevice() {
return getBaseMapper().getAllFloorsWithDevice();
}
FloorMapper
//查询包含智能设备的楼层
List<Floor> getAllFloorsWithDevice();
FloorMapper.xml
<select id="getAllFloorsWithDevice" resultType="com.zzyl.entity.Floor">
select bf.*
from base_floor bf
left join base_room br on br.floor_id = bf.id
left join monitor_device md on md.physical_location_type = 1 and md.binding_location = br.id
left join base_bed bb on bb.room_id = br.id
left join monitor_device md2 on md2.physical_location_type = 2 and md2.binding_location = bb.id
where md.id is not null
or md2.id is not null
group by bf.code
order by bf.code
</select>
- device d关联的是房间的设备,限定条件
d.location_type = 1 and d.physical_location_type = 1
,位置类型为固定,物理位置类为房间 - device dd关联的是床位的设备,限定条件
d.location_type = 1 and d.physical_location_type = 2
,位置类型为固定,物理位置类为床位
获取房间中的智能设备及数据
-- 查询指定楼层下 房间中的设备信息 和 床位中设备信息
select *
from base_room br
left join monitor_device md on md.physical_location_type = 1 and md.binding_location = br.id
left join base_bed bb on bb.room_id = br.id
left join monitor_device md2 on md2.physical_location_type = 2 and md2.binding_location = bb.id
left join elder e on e.bed_id = bb.id
where (md.id is not null or md2.id is not null) and br.floor_id = 1
思路分析
根据楼层查询房间或者是床位的设备数据(最新的一条数据),实现方案有两种:
-
通过楼层关联房间、床位、设备表2次、设备数据表2次共6张表查询对应的数据,此方案设备数据表数据较多,效率不高,不推荐
-
使用缓存,因为只需要查询最近一次的设备数据,可以把最近一次采集的数据存储到redis中,然后让房间或者床位进行匹配
修改AmqpClient类中的processMessage方法,把设备的数据保存到缓存中
@Autowired private DeviceMapper deviceMapper; @Autowired private DeviceDataService deviceDataService; @Autowired private RedisTemplate redisTemplate; /** * 在这里处理您收到消息后的具体业务逻辑。 */ private void processMessage(Message message) { try { byte[] body = message.getBody(byte[].class); String contentStr = new String(body); String topic = message.getStringProperty("topic"); String messageId = message.getStringProperty("messageId"); logger.info("receive message" + ",\n topic = " + topic + ",\n messageId = " + messageId + ",\n content = " + contentStr); //解析数据 Content content = JSONUtil.toBean(contentStr, Content.class); //查询设备是否存在 LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Device::getIotId, content.getIotId()); Device device = deviceMapper.selectOne(wrapper); if (device == null) { logger.error(content.getIotId() + "设备不存在"); return;//结束本次调用 } //从content中获取设备数据,保存到monitor_device_data表 List<DeviceData> list = new ArrayList<>(); content.getItems().forEach((k, v) -> { DeviceData deviceData = new DeviceData(); deviceData.setDeviceName(device.getDeviceName());//设备名称 deviceData.setIotId(content.getIotId());//iotId deviceData.setNickname(device.getNickname());//备注名称 deviceData.setProductKey(device.getProductKey());//设备key deviceData.setProductName(device.getProductName());//设备名称 deviceData.setFunctionId(k);//功能名称 deviceData.setAccessLocation(device.getRemark());//接入位置 deviceData.setLocationType(device.getLocationType());//位置类型 deviceData.setPhysicalLocationType(device.getPhysicalLocationType());//物理位置类型 deviceData.setDeviceDescription(device.getDeviceDescription());//设备描述 deviceData.setAlarmTime(LocalDateTimeUtil.of(v.getTime()));//数据上报时间 deviceData.setDataValue(v.getValue() + "");//数据值 list.add(deviceData); }); //批量保存 deviceDataService.saveBatch(list); //将最新数据覆盖到Redis中 redisTemplate.opsForHash().put("DEVICE_LAST_DATA", content.getIotId(), JSONUtil.toJsonStr(list)); } catch (Exception e) { logger.error("processMessage occurs error ", e); } }
RoomController
//获取所有房间(智能床位)
@GetMapping("/room/getRoomsWithDeviceByFloorId/{floorId}")
public ResponseResult getRoomsWithDeviceByFloorId(@PathVariable(name = "floorId") Long floorId){
List<RoomVo> list = roomService.getRoomsWithDeviceByFloorId(floorId);
return success(list);
}
RoomService
//查询房间及关联的床位和设备
List<RoomVo> getRoomsWithDeviceByFloorId(Long floorId);
RoomServiceImpl
@Autowired
private RedisTemplate redisTemplate;
@Override
public List<RoomVo> getRoomsWithDeviceByFloorId(Long floorId) {
List<RoomVo> roomVoList = getBaseMapper().getRoomsWithDeviceByFloorId(floorId);
roomVoList.forEach(roomVo -> {
//寻找房间中的设备的数据
List<DeviceVo> deviceVos = roomVo.getDeviceVos();
deviceVos.forEach(deviceVo -> {
//查redis中是否包含数据
String deviceDataStr = (String) redisTemplate.opsForHash().get("DEVICE_LAST_DATA", deviceVo.getIotId());
if(StrUtil.isEmpty(deviceDataStr)){
return;//在foreach中return是跳过当前循环
}
List<DeviceDataVo> deviceDataVos = JSONUtil.toList(deviceDataStr, DeviceDataVo.class);
deviceVo.setDeviceDataVos(deviceDataVos);
//设置一个状态,告诉前端设备中有数据
roomVo.setStatus(2);
});
//房间中的床位中的设备的数据
List<BedVo> bedVoList = roomVo.getBedVoList();
bedVoList.forEach(bedVo -> {
bedVo.getDeviceVos().forEach(deviceVo -> {
//查redis中是否包含数据
String deviceDataStr = (String) redisTemplate.opsForHash().get("DEVICE_LAST_DATA", deviceVo.getIotId());
if(StrUtil.isEmpty(deviceDataStr)){
return;//在foreach中return是跳过当前循环
}
List<DeviceDataVo> deviceDataVos = JSONUtil.toList(deviceDataStr, DeviceDataVo.class);
deviceVo.setDeviceDataVos(deviceDataVos);
//设置一个状态,告诉前端设备中有数据
roomVo.setStatus(2);
bedVo.setStatus(2);
});
});
});
return roomVoList;
}
RoomMapper
//查询楼层中的房间和床位设备及数据
List<RoomVo> getRoomsWithDeviceByFloorId(Long floorId);
RoomMapper.xml
<resultMap id="RoomsWithDeviceResult" type="com.zzyl.vo.RoomVo">
<id column="id" property="id"></id>
<result property="code" column="code"/>
<result property="sort" column="sort"/>
<result property="sort" column="sort"/>
<result property="floorId" column="floor_id"/>
<result property="floorName" column="fname"/>
<result column="create_by" property="createBy"/>
<result column="update_by" property="updateBy"/>
<result column="remark" property="remark"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="price" property="price"/>
<collection property="bedVoList" ofType="com.zzyl.vo.BedVo">
<id column="bid" property="id"/>
<result column="bed_number" property="bedNumber"/>
<result column="bed_status" property="bedStatus"/>
<result column="room_id" property="roomId"/>
<result column="ename" property="name"/>
<result column="eid" property="elderId"/>
<collection property="deviceVos" ofType="com.zzyl.vo.DeviceVo">
<id column="b_did" property="id"></id>
<result column="b_iot_id" property="iotId"/>
<result column="b_device_name" property="deviceName"/>
<result column="b_product_key" property="productKey"/>
<result column="b_product_name" property="productName"/>
</collection>
</collection>
<collection property="deviceVos" ofType="com.zzyl.vo.DeviceVo">
<id column="r_did" jdbcType="BIGINT" property="id"/>
<result column="iot_id" jdbcType="VARCHAR" property="iotId"/>
<result column="device_name" jdbcType="VARCHAR" property="deviceName"/>
<result column="product_key" jdbcType="VARCHAR" property="productKey"/>
<result column="product_name" jdbcType="BIT" property="productName"/>
</collection>
</resultMap>
<select id="getRoomsWithDeviceByFloorId" resultMap="RoomsWithDeviceResult">
select br.*,
bb.id as bed_id,
bb.bed_number,
bb.bed_status,
bb.room_id,
e.name as ename,
e.id as eid,
md.id as r_did,
md.iot_id,
md.product_key as product_key,
md.device_name,
md.product_name,
md2.id as b_did,
md2.iot_id b_iot_id,
md2.product_key as b_product_key,
md2.device_name as b_device_name,
md2.product_name as b_product_name
from base_room br
left join monitor_device md on md.physical_location_type = 1 and md.binding_location = br.id
left join base_bed bb on bb.room_id = br.id
left join monitor_device md2 on md2.physical_location_type = 2 and md2.binding_location = bb.id
left join elder e on e.bed_id = bb.id
where (md.id is not null or md2.id is not null) and br.floor_id = #{floorId}
</select>
测试
在绑定设备的床位或房间中,启动设备模拟上报数据
在页面中查询上报之后的数据
小程序数据展示
需求分析
我们先来看原型图
在小程序端,绑定了老人之后,就可以查看老人的健康数据。
- 在家人列表中,点击【健康数据】,跳转到【健康数据】,这里展示的是老人健康数据概览和当下最新指标数据
- 当点击了某一个指标之后,可以查看某一个指标的历史数据,可以按天或周进行报表展示
- 异常数据,后边讲完告警模块之后,再来完善异常数据展示
思路分析
当查看家人列表的时候,需要查询当前登录人的老人列表,同时老人的其他信息也要查询出来(房间、床位、设备)
当点击健康数据的时候,需要根据当前老人绑定的产品和设备名称到IOT平台查询物模型数据
当点击某项指标数据,比如心率,可以查看这个指标数据的报表信息,报表包含了两种
- 按照天查询,其中心率、血压、血氧、体温的传输频次:每3小时测量传输一次,所以一天展示8次
- 按周查询,周数据值显示近七天,天数据为当天数据的平均值
根据上面的思路分析,我们共需要开发4个接口,分别是:家人列表、查询健康数据、按天统计、按周统计
其中的家人列表,在代码中已经提供,我们重点实现剩下的3个
查询健康数据
根据产品ID和设备名称查询物模型数据,我们在前一天已经实现过一次
小程序的查询需要定义在客户管理的控制层,CustomerUserController中实现,如下代码:
@Autowired
private Client client;
@Autowired
private AliIoTConfigProperties aliIoTConfigProperties;
//查询指定设备属性状态
@PostMapping("/customer/user/QueryDevicePropertyStatus")
public ResponseResult QueryDevicePropertyStatus(@RequestBody QueryDevicePropertyStatusRequest request) throws Exception {
request.setIotInstanceId(aliIoTConfigProperties.getIotInstanceId());
QueryDevicePropertyStatusResponse deviceStatus = client.queryDevicePropertyStatus(request);
return ResponseResult.success(deviceStatus.getBody().getData());
}
测试条件有两个:1)家属端登录人需要绑定带有智能手表设备的老人 2)启动模拟数据上报
目前前端只开发了心率的动态展示和报表展示,大家看效果主要看心率这个物模型
按天统计查询指标数据
sql–mapper-controller-service
sql
select DATE_FORMAT(alarm_time,'%H') as date_time, round(avg(data_value),0) as data_value from monitor_device_data
where iot_id = 'ip3hTvIkH8Lmejlad8qkk0sjl0'
and function_id='HeartRate'
and alarm_time between '2024-08-25 00:00:00' and '2024-08-25 23:59:59'
group by date_time
DeviceDataMapper
//按日查询设备数据
List<DeviceDataGraphVo> queryDeviceDataListByDay(String iotId, String functionId, LocalDateTime minCreateTime, LocalDateTime maxCreateTime);
DeviceDataMapper.xml
<select id="queryDeviceDataListByDay" resultType="com.zzyl.vo.DeviceDataGraphVo">
SELECT
DATE_FORMAT(alarm_time, '%H:00') AS date_time,
ROUND(AVG(data_value),0) AS data_value
FROM
monitor_device_data
WHERE
iot_id = #{iotId}
AND function_id = #{functionId}
AND alarm_time BETWEEN #{minCreateTime} and #{maxCreateTime}
GROUP BY
date_time
order by date_time
</select>
AppMemberController
@Autowired
private DeviceDataService deviceDataService;
//按日查询设备数据
@GetMapping("/customer/user/queryDeviceDataListByDay")
public ResponseResult queryDeviceDataListByDay(String iotId, String functionId, Long startTime, Long endTime) {
List<DeviceDataGraphVo> list = deviceDataService.queryDeviceDataListByDay(iotId, functionId, startTime, endTime);
return ResponseResult.success(list);
}
DeviceDataService
//按日查询设备数据
List<DeviceDataGraphVo> queryDeviceDataListByDay(String iotId, String functionId, Long startTime, Long endTime);
DeviceDataServiceImpl
@Override
public List<DeviceDataGraphVo> queryDeviceDataListByDay(String iotId, String functionId, Long startTime, Long endTime) {
//按日聚合数据
List<DeviceDataGraphVo> deviceDataGraphVoList = getBaseMapper().queryDeviceDataListByDay(iotId, functionId, LocalDateTimeUtil.of(startTime), LocalDateTimeUtil.of(endTime));
//转换为map
Map<String, Double> map = deviceDataGraphVoList.stream().collect(Collectors.toMap(DeviceDataGraphVo::getDateTime, DeviceDataGraphVo::getDataValue));
//构建一个24小时的集合
List<DeviceDataGraphVo> list = DeviceDataGraphVo.dayInstance(LocalDateTimeUtil.of(startTime));
list.forEach(d -> {
//从map获取值,如果为空,则给默认为0
Double val = map.get(d.getDateTime()) == null ? 0.0 : map.get(d.getDateTime());
d.setDataValue(val);
});
return list;
}
测试:在小程序端点击心率,可以查看报表数据,也可以选择时间来进行检索,如下图
按周统计查询指标数据
AppMemberController
//按周查询设备数据
@GetMapping("/customer/user/queryDeviceDataListByWeek")
public ResponseResult queryDeviceDataListByWeek(String iotId, String functionId, Long startTime, Long endTime) {
List<DeviceDataGraphVo> list = deviceDataService.queryDeviceDataListByWeek(iotId, functionId, startTime, endTime);
return ResponseResult.success(list);
}
DeviceDataService
//按周查询设备数据
List<DeviceDataGraphVo> queryDeviceDataListByWeek(String iotId, String functionId, Long startTime, Long endTime);
DeviceDataServiceImpl
@Override
public List<DeviceDataGraphVo> queryDeviceDataListByWeek(String iotId, String functionId, Long startTime, Long endTime) {
//按周聚合数据
List<DeviceDataGraphVo> deviceDataGraphVoList = getBaseMapper().queryDeviceDataListByWeek(iotId, functionId, LocalDateTimeUtil.of(startTime), LocalDateTimeUtil.of(endTime));
//转换为map
Map<String, Double> map = deviceDataGraphVoList.stream().collect(Collectors.toMap(DeviceDataGraphVo::getDateTime, DeviceDataGraphVo::getDataValue));
//构建一个7天的集合
List<DeviceDataGraphVo> list = DeviceDataGraphVo.weekInstance(LocalDateTimeUtil.of(startTime));
list.forEach(d -> {
//获取数据,如果为空,则补为0
Double dataValue = map.get(d.getDateTime()) == null ? 0.0 : map.get(d.getDateTime());
d.setDataValue(dataValue);
});
return list;
DeviceDataMapper
//按周查询设备数据
List<DeviceDataGraphVo> queryDeviceDataListByWeek(String iotId, String functionId, LocalDateTime minCreateTime, LocalDateTime maxCreateTime);
DeviceDataMapper.xml
<select id="queryDeviceDataListByWeek" resultType="com.zzyl.vo.DeviceDataGraphVo">
SELECT
DATE_FORMAT(alarm_time, '%m.%d') AS date_time,
ROUND(AVG(data_value),0) AS data_value
FROM
monitor_device_data
WHERE
iot_id = #{iotId}
AND function_id = #{functionId}
AND alarm_time BETWEEN #{minCreateTime} and #{maxCreateTime}
GROUP BY
date_time
order by date_time
</select>
作业
Map<String, Double> map = deviceDataGraphVoList.stream().collect(Collectors.toMap(DeviceDataGraphVo::getDateTime, DeviceDataGraphVo::getDataValue));
//构建一个7天的集合
List<DeviceDataGraphVo> list = DeviceDataGraphVo.weekInstance(LocalDateTimeUtil.of(startTime));
list.forEach(d -> {
//获取数据,如果为空,则补为0
Double dataValue = map.get(d.getDateTime()) == null ? 0.0 : map.get(d.getDateTime());
d.setDataValue(dataValue);
});
return list;
### DeviceDataMapper
```java
//按周查询设备数据
List<DeviceDataGraphVo> queryDeviceDataListByWeek(String iotId, String functionId, LocalDateTime minCreateTime, LocalDateTime maxCreateTime);
DeviceDataMapper.xml
<select id="queryDeviceDataListByWeek" resultType="com.zzyl.vo.DeviceDataGraphVo">
SELECT
DATE_FORMAT(alarm_time, '%m.%d') AS date_time,
ROUND(AVG(data_value),0) AS data_value
FROM
monitor_device_data
WHERE
iot_id = #{iotId}
AND function_id = #{functionId}
AND alarm_time BETWEEN #{minCreateTime} and #{maxCreateTime}
GROUP BY
date_time
order by date_time
</select>
[外链图片转存中…(img-nLTHAHPo-1724850575368)]
作业
定时任务 每天凌晨一点 删除上报时间超过7天