| | |
| | | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| | | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| | | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; |
| | | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| | | import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; |
| | | import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty; |
| | | import cn.iocoder.yudao.module.iot.dal.redis.device.DevicePropertyRedisDAO; |
| | | import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper; |
| | | import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; |
| | | import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; |
| | | import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; |
| | | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| | | import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.ArgumentCaptor; |
| | |
| | | import org.mockito.Mock; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static java.util.Collections.singletonList; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | |
| | | @Mock |
| | | private IotThingModelService thingModelService; |
| | | @Mock |
| | | private IotProductService productService; |
| | | @Mock |
| | | private IotDevicePropertyMapper devicePropertyMapper; |
| | | @Mock |
| | | private DevicePropertyRedisDAO deviceDataRedisDAO; |
| | |
| | | // mock 行为 |
| | | when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) |
| | | .thenReturn(singletonList(thingModel)); |
| | | when(thingModelService.convertThingModelPropertyValue(thingModel, 100)).thenReturn(100); |
| | | |
| | | // 调用 |
| | | service.saveDeviceProperty(device, message); |
| | |
| | | } |
| | | |
| | | @Test |
| | | public void testSaveDeviceProperty_boolFromBooleanTrue() { |
| | | // 准备参数:物模型为 BOOL,设备上报原生 boolean true |
| | | assertBoolValueConvertedToByte(true, (byte) 1); |
| | | } |
| | | |
| | | @Test |
| | | public void testSaveDeviceProperty_boolFromBooleanFalse() { |
| | | // 准备参数:物模型为 BOOL,设备上报原生 boolean false |
| | | assertBoolValueConvertedToByte(false, (byte) 0); |
| | | } |
| | | |
| | | @Test |
| | | public void testSaveDeviceProperty_boolFromStringTrue() { |
| | | // 准备参数:物模型为 BOOL,设备上报字符串 "true" |
| | | assertBoolValueConvertedToByte("true", (byte) 1); |
| | | } |
| | | |
| | | @Test |
| | | public void testSaveDeviceProperty_boolFromStringFalse() { |
| | | // 准备参数:物模型为 BOOL,设备上报字符串 "false" |
| | | assertBoolValueConvertedToByte("false", (byte) 0); |
| | | } |
| | | |
| | | @Test |
| | | public void testSaveDeviceProperty_boolFromNumberOne() { |
| | | // 准备参数:物模型为 BOOL,设备上报数字 1 |
| | | assertBoolValueConvertedToByte(1, (byte) 1); |
| | | } |
| | | |
| | | @Test |
| | | public void testSaveDeviceProperty_boolFromNumberZero() { |
| | | // 准备参数:物模型为 BOOL,设备上报数字 0 |
| | | assertBoolValueConvertedToByte(0, (byte) 0); |
| | | } |
| | | |
| | | /** |
| | | * 校验 BOOL 类型属性上报后,最终落到 properties Map 的值类型与数值 |
| | | */ |
| | | private void assertBoolValueConvertedToByte(Object reportedValue, byte expected) { |
| | | // 准备参数 |
| | | public void testSaveDeviceProperty_convertValueFailed() { |
| | | // 准备参数:物模型存在,但是属性值无法按物模型转换 |
| | | IotDeviceDO device = buildDevice(); |
| | | IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType()); |
| | | IotThingModelDO temperature = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType()); |
| | | Map<String, Object> params = new HashMap<>(); |
| | | params.put("PowerSwitch", reportedValue); |
| | | params.put("Temperature", "abc"); |
| | | IotDeviceMessage message = buildMessage(params); |
| | | |
| | | // mock 行为 |
| | | // mock 方法 |
| | | when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) |
| | | .thenReturn(singletonList(temperature)); |
| | | when(thingModelService.convertThingModelPropertyValue(temperature, "abc")).thenReturn(null); |
| | | |
| | | // 调用,并断言:不会抛出异常 |
| | | assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); |
| | | |
| | | // 断言:没有合法属性,不会写入 TDengine 与 Redis |
| | | verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong()); |
| | | verify(deviceDataRedisDAO, never()).putAll(anyLong(), any()); |
| | | } |
| | | |
| | | @Test |
| | | public void testSaveDeviceProperty_skipNullValue() { |
| | | // 准备参数:属性值为空,不能写入 TDengine 与 Redis |
| | | IotDeviceDO device = buildDevice(); |
| | | IotThingModelDO thingModel = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType()); |
| | | Map<String, Object> params = new HashMap<>(); |
| | | params.put("Temperature", null); |
| | | IotDeviceMessage message = buildMessage(params); |
| | | |
| | | // mock 方法 |
| | | when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) |
| | | .thenReturn(singletonList(thingModel)); |
| | | |
| | | // 调用:不能抛异常 |
| | | // 调用,并断言:不会抛出异常 |
| | | assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); |
| | | |
| | | // 断言:写入的 value 是 byte 类型,且值匹配 |
| | | // 断言:跳过空值,不会转换属性值,也不会写入 TDengine 与 Redis |
| | | verify(thingModelService, never()).convertThingModelPropertyValue(any(), any()); |
| | | verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong()); |
| | | verify(deviceDataRedisDAO, never()).putAll(anyLong(), any()); |
| | | } |
| | | |
| | | @Test |
| | | public void testSaveDeviceProperty_skipInvalidKeyType() { |
| | | // 准备参数:Map 中包含非字符串 key,不能因为强转失败影响其它合法属性 |
| | | IotDeviceDO device = buildDevice(); |
| | | IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType()); |
| | | Map<Object, Object> params = new HashMap<>(); |
| | | params.put(123, 1); |
| | | params.put("PowerSwitch", true); |
| | | IotDeviceMessage message = buildMessage(params); |
| | | |
| | | // mock 方法 |
| | | when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) |
| | | .thenReturn(singletonList(thingModel)); |
| | | when(thingModelService.convertThingModelPropertyValue(thingModel, true)).thenReturn((byte) 1); |
| | | |
| | | // 调用,并断言:非字符串 key 不影响其它合法属性 |
| | | assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); |
| | | |
| | | // 断言:只写入合法属性 |
| | | Map<String, Object> dbProperties = captureMapperInsertProperties(); |
| | | Object actual = dbProperties.get("PowerSwitch"); |
| | | assertTrue(actual instanceof Byte, "BOOL 属性应被转为 Byte 类型,实际为 " + (actual == null ? "null" : actual.getClass())); |
| | | assertEquals(expected, actual); |
| | | assertEquals(1, dbProperties.size()); |
| | | assertEquals((byte) 1, dbProperties.get("PowerSwitch")); |
| | | } |
| | | |
| | | @Test |
| | | public void testDefineDevicePropertyData_fieldNameToLowerCase() { |
| | | // 准备参数:全大写缩写和驼峰缩写都需要转换为 TDengine 实际的小写字段名 |
| | | Long productId = 2L; |
| | | IotProductDO product = IotProductDO.builder().id(productId).build(); |
| | | List<IotThingModelDO> thingModels = Arrays.asList( |
| | | buildThingModel("Ua", IotDataSpecsDataTypeEnum.FLOAT.getDataType()), |
| | | buildThingModel("PfT", IotDataSpecsDataTypeEnum.FLOAT.getDataType()), |
| | | buildThingModel("PT", IotDataSpecsDataTypeEnum.FLOAT.getDataType()), |
| | | buildThingModel("PA", IotDataSpecsDataTypeEnum.FLOAT.getDataType())); |
| | | thingModels.forEach(thingModel -> thingModel.setType(IotThingModelTypeEnum.PROPERTY.getType())); |
| | | |
| | | // mock 方法 |
| | | when(productService.validateProductExists(productId)).thenReturn(product); |
| | | when(thingModelService.getThingModelListByProductId(productId)).thenReturn(thingModels); |
| | | when(devicePropertyMapper.getProductPropertySTableFieldList(productId)).thenReturn(Collections.emptyList()); |
| | | |
| | | // 调用 |
| | | service.defineDevicePropertyData(productId); |
| | | |
| | | // 断言:字段名统一为小写下划线,避免 PT 和数据库中的 pt 被误判为不同字段 |
| | | ArgumentCaptor<List<TDengineTableField>> captor = ArgumentCaptor.forClass(List.class); |
| | | verify(devicePropertyMapper).createProductPropertySTable(eq(productId), captor.capture()); |
| | | assertEquals(Arrays.asList("ua", "pf_t", "pt", "pa"), captor.getValue().stream() |
| | | .map(TDengineTableField::getField) |
| | | .collect(Collectors.toList())); |
| | | } |
| | | |
| | | // ========== 辅助方法 ========== |
| | |
| | | /** |
| | | * 构造一条属性上报消息 |
| | | */ |
| | | private IotDeviceMessage buildMessage(Map<String, Object> params) { |
| | | private IotDeviceMessage buildMessage(Map<?, ?> params) { |
| | | IotDeviceMessage message = new IotDeviceMessage(); |
| | | message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); |
| | | message.setParams(params); |