添加IOT接口
This commit is contained in:
parent
34f86b6329
commit
9fd9602718
5
pom.xml
5
pom.xml
|
|
@ -17,6 +17,11 @@
|
|||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.thingsboard</groupId>
|
||||
<artifactId>rest-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Tuoheng Device API -->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package com.ruoyi.device.domain.api;
|
||||
|
||||
import com.ruoyi.device.domain.model.thingsboard.AttributeMap;
|
||||
import com.ruoyi.device.domain.model.thingsboard.DeviceInfo;
|
||||
import com.ruoyi.device.domain.model.thingsboard.TelemetryMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ThingsBoard设备服务接口
|
||||
* 提供类型安全的设备查询功能
|
||||
*/
|
||||
public interface IThingsBoardDomain {
|
||||
|
||||
/**
|
||||
* 获取所有设备的迭代器
|
||||
* 每次迭代返回一页设备列表
|
||||
*
|
||||
* @return 设备迭代器
|
||||
*/
|
||||
Iterable<List<DeviceInfo>> getAllDevices();
|
||||
|
||||
/**
|
||||
* 根据设备ID获取设备的所有属性
|
||||
* 只返回已注册的属性键对应的数据,未注册的键会被忽略
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 类型安全的属性映射
|
||||
*/
|
||||
AttributeMap getDeviceAttributes(String deviceId);
|
||||
|
||||
/**
|
||||
* 根据设备ID获取设备的所有遥测数据
|
||||
* 只返回已注册的遥测键对应的数据,未注册的键会被忽略
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 类型安全的遥测数据映射
|
||||
*/
|
||||
TelemetryMap getDeviceTelemetry(String deviceId);
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package com.ruoyi.device.domain.impl;
|
||||
|
||||
|
||||
import com.ruoyi.device.domain.api.IThingsBoardDomain;
|
||||
import com.ruoyi.device.domain.model.thingsboard.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.thingsboard.rest.client.RestClient;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
|
||||
import org.thingsboard.server.common.data.kv.TsKvEntry;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* ThingsBoard设备服务实现类
|
||||
*/
|
||||
public class ThingsBoardDomainImpl implements IThingsBoardDomain {
|
||||
private static final Logger log = LoggerFactory.getLogger(ThingsBoardDomainImpl.class);
|
||||
|
||||
private final RestClient client;
|
||||
private final int pageSize;
|
||||
|
||||
public ThingsBoardDomainImpl(RestClientManager clientManager) {
|
||||
this(clientManager, 10);
|
||||
}
|
||||
|
||||
public ThingsBoardDomainImpl(RestClientManager clientManager, int pageSize) {
|
||||
this.client = clientManager.getClient();
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<List<DeviceInfo>> getAllDevices() {
|
||||
return new DeviceIterator(client, pageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributeMap getDeviceAttributes(String deviceId) {
|
||||
AttributeMap attributeMap = new AttributeMap();
|
||||
|
||||
try {
|
||||
DeviceId id = new DeviceId(UUID.fromString(deviceId));
|
||||
|
||||
// 获取所有属性键
|
||||
List<String> attributeKeys = client.getAttributeKeys(id);
|
||||
if (attributeKeys == null || attributeKeys.isEmpty()) {
|
||||
log.debug("设备 {} 没有属性", deviceId);
|
||||
return attributeMap;
|
||||
}
|
||||
|
||||
// 获取属性值
|
||||
List<AttributeKvEntry> attributeKvEntries = client.getAttributeKvEntries(id, attributeKeys);
|
||||
if (attributeKvEntries == null || attributeKvEntries.isEmpty()) {
|
||||
log.debug("设备 {} 的属性值为空", deviceId);
|
||||
return attributeMap;
|
||||
}
|
||||
|
||||
// 解析并填充到AttributeMap
|
||||
for (AttributeKvEntry entry : attributeKvEntries) {
|
||||
parseAndPutAttribute(attributeMap, entry);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取设备属性失败: deviceId={}, error={}", deviceId, e.getMessage(), e);
|
||||
}
|
||||
|
||||
return attributeMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TelemetryMap getDeviceTelemetry(String deviceId) {
|
||||
TelemetryMap telemetryMap = new TelemetryMap();
|
||||
|
||||
try {
|
||||
DeviceId id = new DeviceId(UUID.fromString(deviceId));
|
||||
|
||||
// 获取所有遥测键
|
||||
List<String> timeseriesKeys = client.getTimeseriesKeys(id);
|
||||
if (timeseriesKeys == null || timeseriesKeys.isEmpty()) {
|
||||
log.debug("设备 {} 没有遥测数据", deviceId);
|
||||
return telemetryMap;
|
||||
}
|
||||
|
||||
// 获取最新的遥测数据
|
||||
List<TsKvEntry> latestTimeseries = client.getLatestTimeseries(id, timeseriesKeys);
|
||||
if (latestTimeseries == null || latestTimeseries.isEmpty()) {
|
||||
log.debug("设备 {} 的遥测数据为空", deviceId);
|
||||
return telemetryMap;
|
||||
}
|
||||
|
||||
// 解析并填充到TelemetryMap
|
||||
for (TsKvEntry entry : latestTimeseries) {
|
||||
parseAndPutTelemetry(telemetryMap, entry);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取设备遥测数据失败: deviceId={}, error={}", deviceId, e.getMessage(), e);
|
||||
}
|
||||
|
||||
return telemetryMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析属性并添加到AttributeMap
|
||||
* 使用延迟注册机制,自动处理所有属性
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void parseAndPutAttribute(AttributeMap attributeMap, AttributeKvEntry entry) {
|
||||
String keyName = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
try {
|
||||
// 使用延迟注册机制:如果键不存在则自动创建
|
||||
AttributeKey<?> key = AttributeKey.getOrCreate(keyName, value);
|
||||
|
||||
// 使用键的解析器解析值
|
||||
Object parsedValue = ((AttributeKey<Object>) key).parse(value);
|
||||
attributeMap.put((AttributeKey<Object>) key, parsedValue);
|
||||
log.debug("成功解析属性: {} = {} (type: {})", keyName, parsedValue, key.getType().getSimpleName());
|
||||
} catch (Exception e) {
|
||||
log.warn("解析属性失败: key={}, value={}, error={}", keyName, value, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析遥测数据并添加到TelemetryMap
|
||||
* 使用延迟注册机制,自动处理所有遥测数据
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void parseAndPutTelemetry(TelemetryMap telemetryMap, TsKvEntry entry) {
|
||||
String keyName = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
try {
|
||||
// 使用延迟注册机制:如果键不存在则自动创建
|
||||
TelemetryKey<?> key = TelemetryKey.getOrCreate(keyName, value);
|
||||
|
||||
// 使用键的解析器解析值
|
||||
Object parsedValue = ((TelemetryKey<Object>) key).parse(value);
|
||||
telemetryMap.put((TelemetryKey<Object>) key, parsedValue, entry.getTs());
|
||||
log.debug("成功解析遥测数据: {} = {} (timestamp: {}, type: {})",
|
||||
keyName, parsedValue, entry.getTs(), key.getType().getSimpleName());
|
||||
} catch (Exception e) {
|
||||
log.warn("解析遥测数据失败: key={}, value={}, error={}", keyName, value, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.ruoyi.device.domain.model.thingsboard;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 类型安全的属性键
|
||||
* 使用泛型确保类型安全,避免使用Object作为返回值
|
||||
*
|
||||
* @param <T> 属性值的类型
|
||||
*/
|
||||
public class AttributeKey<T> {
|
||||
private final String name;
|
||||
private final Class<T> type;
|
||||
private final ValueParser<T> parser;
|
||||
|
||||
private static final Map<String, AttributeKey<?>> REGISTRY = new HashMap<>();
|
||||
|
||||
private AttributeKey(String name, Class<T> type, ValueParser<T> parser) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建属性键并注册
|
||||
*/
|
||||
public static <T> AttributeKey<T> of(String name, Class<T> type, ValueParser<T> parser) {
|
||||
AttributeKey<T> key = new AttributeKey<>(name, type, parser);
|
||||
REGISTRY.put(name, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称查找已注册的属性键
|
||||
*/
|
||||
public static Optional<AttributeKey<?>> findByName(String name) {
|
||||
return Optional.ofNullable(REGISTRY.get(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建属性键(延迟注册)
|
||||
* 如果键已存在则返回,否则根据值自动推断类型并创建
|
||||
*
|
||||
* @param name 属性名称
|
||||
* @param value 属性值
|
||||
* @return 属性键
|
||||
*/
|
||||
public static AttributeKey<?> getOrCreate(String name, Object value) {
|
||||
return REGISTRY.computeIfAbsent(name, k -> inferKeyFromValue(name, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析原始值为目标类型
|
||||
*/
|
||||
public T parse(Object rawValue) {
|
||||
return parser.parse(rawValue);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Class<T> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AttributeKey{" +
|
||||
"name='" + name + '\'' +
|
||||
", type=" + type.getSimpleName() +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据值自动推断类型并创建属性键
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> AttributeKey<T> inferKeyFromValue(String name, Object value) {
|
||||
if (value == null) {
|
||||
// 默认为 String 类型
|
||||
return (AttributeKey<T>) of(name, String.class, v -> v != null ? v.toString() : null);
|
||||
}
|
||||
|
||||
// 根据值的实际类型推断
|
||||
if (value instanceof Boolean) {
|
||||
return (AttributeKey<T>) of(name, Boolean.class, v -> {
|
||||
if (v == null) return null;
|
||||
if (v instanceof Boolean) return (Boolean) v;
|
||||
return Boolean.parseBoolean(v.toString());
|
||||
});
|
||||
} else if (value instanceof Long || value instanceof Integer) {
|
||||
return (AttributeKey<T>) of(name, Long.class, v -> {
|
||||
if (v == null) return null;
|
||||
if (v instanceof Number) return ((Number) v).longValue();
|
||||
return Long.parseLong(v.toString());
|
||||
});
|
||||
} else if (value instanceof Double || value instanceof Float) {
|
||||
return (AttributeKey<T>) of(name, Double.class, v -> {
|
||||
if (v == null) return null;
|
||||
if (v instanceof Number) return ((Number) v).doubleValue();
|
||||
return Double.parseDouble(v.toString());
|
||||
});
|
||||
} else {
|
||||
// 默认为 String 类型
|
||||
return (AttributeKey<T>) of(name, String.class, v -> v != null ? v.toString() : null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 值解析器接口
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ValueParser<T> {
|
||||
T parse(Object rawValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.ruoyi.device.domain.model.thingsboard;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 类型安全的属性映射
|
||||
* 解决了使用Object作为返回值的问题
|
||||
*/
|
||||
public class AttributeMap {
|
||||
private final Map<AttributeKey<?>, Object> data = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 添加属性值(类型安全)
|
||||
*/
|
||||
public <T> void put(AttributeKey<T> key, T value) {
|
||||
data.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性值(类型安全)
|
||||
* 返回Optional避免null问题
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<T> get(AttributeKey<T> key) {
|
||||
return Optional.ofNullable((T) data.get(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性值,如果不存在返回默认值
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getOrDefault(AttributeKey<T> key, T defaultValue) {
|
||||
return (T) data.getOrDefault(key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含某个键
|
||||
*/
|
||||
public boolean containsKey(AttributeKey<?> key) {
|
||||
return data.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有键
|
||||
*/
|
||||
public Set<AttributeKey<?>> keySet() {
|
||||
return data.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性数量
|
||||
*/
|
||||
public int size() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为空
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return data.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("AttributeMap{\n");
|
||||
data.forEach((key, value) ->
|
||||
sb.append(" ").append(key.getName()).append(": ").append(value).append("\n")
|
||||
);
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.ruoyi.device.domain.model.thingsboard;
|
||||
|
||||
/**
|
||||
* 类型安全的属性值包装类
|
||||
* 避免使用Object作为返回值
|
||||
*
|
||||
* @param <T> 值的类型
|
||||
*/
|
||||
public class AttributeValue<T> {
|
||||
private final AttributeKey<T> key;
|
||||
private final T value;
|
||||
|
||||
public AttributeValue(AttributeKey<T> key, T value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public AttributeKey<T> getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getKeyName() {
|
||||
return key.getName();
|
||||
}
|
||||
|
||||
public Class<T> getType() {
|
||||
return key.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return key.getName() + ": " + value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.ruoyi.device.domain.model.thingsboard;
|
||||
|
||||
/**
|
||||
* 预定义的设备属性键
|
||||
* 使用类型安全的方式定义常用属性
|
||||
*/
|
||||
public class DeviceAttributes {
|
||||
|
||||
// 连接器类型 - String
|
||||
public static final AttributeKey<String> CONNECTOR_TYPE = AttributeKey.of(
|
||||
"connectorType",
|
||||
String.class,
|
||||
value -> value != null ? value.toString() : null
|
||||
);
|
||||
|
||||
// 连接器名称 - String
|
||||
public static final AttributeKey<String> CONNECTOR_NAME = AttributeKey.of(
|
||||
"connectorName",
|
||||
String.class,
|
||||
value -> value != null ? value.toString() : null
|
||||
);
|
||||
|
||||
// 网关 - String
|
||||
public static final AttributeKey<String> GATEWAY = AttributeKey.of(
|
||||
"gateway",
|
||||
String.class,
|
||||
value -> value != null ? value.toString() : null
|
||||
);
|
||||
|
||||
// 最后连接时间 - Long
|
||||
public static final AttributeKey<Long> LAST_CONNECT_TIME = AttributeKey.of(
|
||||
"lastConnectTime",
|
||||
Long.class,
|
||||
value -> {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
return Long.parseLong(value.toString());
|
||||
}
|
||||
);
|
||||
|
||||
// 是否激活 - Boolean
|
||||
public static final AttributeKey<Boolean> ACTIVE = AttributeKey.of(
|
||||
"active",
|
||||
Boolean.class,
|
||||
value -> {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
return Boolean.parseBoolean(value.toString());
|
||||
}
|
||||
);
|
||||
|
||||
// 最后活动时间 - Long
|
||||
public static final AttributeKey<Long> LAST_ACTIVITY_TIME = AttributeKey.of(
|
||||
"lastActivityTime",
|
||||
Long.class,
|
||||
value -> {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
return Long.parseLong(value.toString());
|
||||
}
|
||||
);
|
||||
|
||||
// 最后断开连接时间 - Long
|
||||
public static final AttributeKey<Long> LAST_DISCONNECT_TIME = AttributeKey.of(
|
||||
"lastDisconnectTime",
|
||||
Long.class,
|
||||
value -> {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
return Long.parseLong(value.toString());
|
||||
}
|
||||
);
|
||||
|
||||
private DeviceAttributes() {
|
||||
// 工具类,禁止实例化
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有属性键
|
||||
* 确保所有静态字段被加载和注册
|
||||
*/
|
||||
public static void init() {
|
||||
// 这个方法的存在是为了触发类的静态初始化
|
||||
// 当调用此方法时,所有静态字段都会被初始化
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.ruoyi.device.domain.model.thingsboard;
|
||||
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
|
||||
/**
|
||||
* 设备信息
|
||||
*/
|
||||
public class DeviceInfo {
|
||||
private final String name;
|
||||
private final String id;
|
||||
private final String type;
|
||||
private final DeviceId deviceId;
|
||||
|
||||
public DeviceInfo(String name, String id, String type, DeviceId deviceId) {
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public DeviceId getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DeviceInfo{" +
|
||||
"name='" + name + '\'' +
|
||||
", id='" + id + '\'' +
|
||||
", type='" + type + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.ruoyi.device.domain.model.thingsboard;
|
||||
|
||||
import org.thingsboard.rest.client.RestClient;
|
||||
import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.page.PageData;
|
||||
import org.thingsboard.server.common.data.page.PageLink;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* 设备迭代器
|
||||
* 支持分页遍历所有设备
|
||||
*/
|
||||
public class DeviceIterator implements Iterable<List<DeviceInfo>> {
|
||||
|
||||
private final RestClient client;
|
||||
private final int pageSize;
|
||||
|
||||
public DeviceIterator(RestClient client, int pageSize) {
|
||||
this.client = client;
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<List<DeviceInfo>> iterator() {
|
||||
return new Iterator<List<DeviceInfo>>() {
|
||||
private PageLink pageLink = new PageLink(pageSize);
|
||||
private PageData<Device> currentPage = null;
|
||||
private boolean hasNextPage = true;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return hasNextPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DeviceInfo> next() {
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException("No more devices");
|
||||
}
|
||||
|
||||
// 获取当前页数据
|
||||
currentPage = client.getTenantDevices("", pageLink);
|
||||
|
||||
// 转换为DeviceInfo列表
|
||||
List<DeviceInfo> deviceInfoList = currentPage.getData().stream()
|
||||
.map(device -> new DeviceInfo(
|
||||
device.getName(),
|
||||
device.getId().getId().toString(),
|
||||
device.getType(),
|
||||
device.getId()
|
||||
))
|
||||
.toList();
|
||||
|
||||
// 准备下一页
|
||||
if (currentPage.hasNext()) {
|
||||
pageLink = pageLink.nextPageLink();
|
||||
} else {
|
||||
hasNextPage = false;
|
||||
}
|
||||
|
||||
return deviceInfoList;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.ruoyi.device.domain.model.thingsboard;
|
||||
|
||||
/**
|
||||
* 预定义的设备遥测数据键
|
||||
* 使用类型安全的方式定义常用遥测数据
|
||||
*/
|
||||
public class DeviceTelemetry {
|
||||
|
||||
// 空调状态 - Integer
|
||||
public static final TelemetryKey<Integer> AIR_CONDITIONER_STATE = TelemetryKey.of(
|
||||
"air_conditioner.air_conditioner_state",
|
||||
Integer.class,
|
||||
value -> {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
return Integer.parseInt(value.toString());
|
||||
}
|
||||
);
|
||||
|
||||
// 温度 - Double
|
||||
public static final TelemetryKey<Double> TEMPERATURE = TelemetryKey.of(
|
||||
"temperature",
|
||||
Double.class,
|
||||
value -> {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
}
|
||||
return Double.parseDouble(value.toString());
|
||||
}
|
||||
);
|
||||
|
||||
// 湿度 - Double
|
||||
public static final TelemetryKey<Double> HUMIDITY = TelemetryKey.of(
|
||||
"humidity",
|
||||
Double.class,
|
||||
value -> {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
}
|
||||
return Double.parseDouble(value.toString());
|
||||
}
|
||||
);
|
||||
|
||||
private DeviceTelemetry() {
|
||||
// 工具类,禁止实例化
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有遥测键
|
||||
* 确保所有静态字段被加载和注册
|
||||
*/
|
||||
public static void init() {
|
||||
// 这个方法的存在是为了触发类的静态初始化
|
||||
// 当调用此方法时,所有静态字段都会被初始化
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.ruoyi.device.domain.model.thingsboard;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.thingsboard.rest.client.RestClient;
|
||||
|
||||
/**
|
||||
* RestClient单例管理器
|
||||
* 提供全局唯一的RestClient实例,避免重复创建连接
|
||||
*
|
||||
* 注意:RestClient内部已经实现了token自动刷新和重新登录机制,
|
||||
* 本管理器主要用于全局共享同一个连接实例
|
||||
*/
|
||||
public class RestClientManager {
|
||||
private static final Logger log = LoggerFactory.getLogger(RestClientManager.class);
|
||||
|
||||
private static volatile RestClientManager instance;
|
||||
private volatile RestClient client;
|
||||
|
||||
private final String url;
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
/**
|
||||
* 私有构造函数
|
||||
*/
|
||||
private RestClientManager(String url, String username, String password) {
|
||||
this.url = url;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例(双重检查锁)
|
||||
*/
|
||||
public static RestClientManager getInstance(String url, String username, String password) {
|
||||
if (instance == null) {
|
||||
synchronized (RestClientManager.class) {
|
||||
if (instance == null) {
|
||||
instance = new RestClientManager(url, username, password);
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取RestClient实例
|
||||
* 懒加载:第一次调用时才创建并登录
|
||||
*
|
||||
* 注意:RestClient内部已实现token自动刷新机制,
|
||||
* 即使长时间不使用,下次请求时也会自动处理过期问题
|
||||
*/
|
||||
public RestClient getClient() {
|
||||
if (client == null) {
|
||||
synchronized (this) {
|
||||
if (client == null) {
|
||||
initClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化并登录客户端
|
||||
*/
|
||||
private void initClient() {
|
||||
try {
|
||||
log.info("正在连接到ThingsBoard: {}", url);
|
||||
client = new RestClient(url);
|
||||
client.login(username, password);
|
||||
log.info("登录成功");
|
||||
} catch (Exception e) {
|
||||
log.error("登录失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("无法连接到ThingsBoard服务器", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭客户端连接
|
||||
* 注意:关闭后需要重新获取实例才能使用
|
||||
*/
|
||||
public synchronized void close() {
|
||||
if (client != null) {
|
||||
try {
|
||||
client.logout();
|
||||
client.close();
|
||||
log.info("客户端已关闭");
|
||||
} catch (Exception e) {
|
||||
log.error("关闭客户端时出错: {}", e.getMessage(), e);
|
||||
} finally {
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.ruoyi.device.domain.model.thingsboard;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 类型安全的遥测数据键
|
||||
* 包含时间戳信息
|
||||
*
|
||||
* @param <T> 遥测值的类型
|
||||
*/
|
||||
public class TelemetryKey<T> {
|
||||
private final String name;
|
||||
private final Class<T> type;
|
||||
private final ValueParser<T> parser;
|
||||
|
||||
private static final Map<String, TelemetryKey<?>> REGISTRY = new HashMap<>();
|
||||
|
||||
private TelemetryKey(String name, Class<T> type, ValueParser<T> parser) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建遥测键并注册
|
||||
*/
|
||||
public static <T> TelemetryKey<T> of(String name, Class<T> type, ValueParser<T> parser) {
|
||||
TelemetryKey<T> key = new TelemetryKey<>(name, type, parser);
|
||||
REGISTRY.put(name, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称查找已注册的遥测键
|
||||
*/
|
||||
public static Optional<TelemetryKey<?>> findByName(String name) {
|
||||
return Optional.ofNullable(REGISTRY.get(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建遥测键(延迟注册)
|
||||
* 如果键已存在则返回,否则根据值自动推断类型并创建
|
||||
*
|
||||
* @param name 遥测数据名称
|
||||
* @param value 遥测数据值
|
||||
* @return 遥测键
|
||||
*/
|
||||
public static TelemetryKey<?> getOrCreate(String name, Object value) {
|
||||
return REGISTRY.computeIfAbsent(name, k -> inferKeyFromValue(name, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析原始值为目标类型
|
||||
*/
|
||||
public T parse(Object rawValue) {
|
||||
return parser.parse(rawValue);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Class<T> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TelemetryKey{" +
|
||||
"name='" + name + '\'' +
|
||||
", type=" + type.getSimpleName() +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据值自动推断类型并创建遥测键
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> TelemetryKey<T> inferKeyFromValue(String name, Object value) {
|
||||
if (value == null) {
|
||||
// 默认为 String 类型
|
||||
return (TelemetryKey<T>) of(name, String.class, v -> v != null ? v.toString() : null);
|
||||
}
|
||||
|
||||
// 根据值的实际类型推断
|
||||
if (value instanceof Boolean) {
|
||||
return (TelemetryKey<T>) of(name, Boolean.class, v -> {
|
||||
if (v == null) return null;
|
||||
if (v instanceof Boolean) return (Boolean) v;
|
||||
return Boolean.parseBoolean(v.toString());
|
||||
});
|
||||
} else if (value instanceof Long || value instanceof Integer) {
|
||||
return (TelemetryKey<T>) of(name, Long.class, v -> {
|
||||
if (v == null) return null;
|
||||
if (v instanceof Number) return ((Number) v).longValue();
|
||||
return Long.parseLong(v.toString());
|
||||
});
|
||||
} else if (value instanceof Double || value instanceof Float) {
|
||||
return (TelemetryKey<T>) of(name, Double.class, v -> {
|
||||
if (v == null) return null;
|
||||
if (v instanceof Number) return ((Number) v).doubleValue();
|
||||
return Double.parseDouble(v.toString());
|
||||
});
|
||||
} else {
|
||||
// 默认为 String 类型
|
||||
return (TelemetryKey<T>) of(name, String.class, v -> v != null ? v.toString() : null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 值解析器接口
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ValueParser<T> {
|
||||
T parse(Object rawValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.ruoyi.device.domain.model.thingsboard;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 类型安全的遥测数据映射
|
||||
*/
|
||||
public class TelemetryMap {
|
||||
private final Map<TelemetryKey<?>, TelemetryValue<?>> data = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 添加遥测值(类型安全)
|
||||
*/
|
||||
public <T> void put(TelemetryKey<T> key, T value, long timestamp) {
|
||||
data.put(key, new TelemetryValue<>(key, value, timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取遥测值(类型安全)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<TelemetryValue<T>> get(TelemetryKey<T> key) {
|
||||
return Optional.ofNullable((TelemetryValue<T>) data.get(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含某个键
|
||||
*/
|
||||
public boolean containsKey(TelemetryKey<?> key) {
|
||||
return data.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有键
|
||||
*/
|
||||
public Set<TelemetryKey<?>> keySet() {
|
||||
return data.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据数量
|
||||
*/
|
||||
public int size() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为空
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return data.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("TelemetryMap{\n");
|
||||
data.forEach((key, value) ->
|
||||
sb.append(" ").append(value.toString()).append("\n")
|
||||
);
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.ruoyi.device.domain.model.thingsboard;
|
||||
|
||||
/**
|
||||
* 类型安全的遥测数据值包装类
|
||||
* 包含时间戳信息
|
||||
*
|
||||
* @param <T> 值的类型
|
||||
*/
|
||||
public class TelemetryValue<T> {
|
||||
private final TelemetryKey<T> key;
|
||||
private final T value;
|
||||
private final long timestamp;
|
||||
|
||||
public TelemetryValue(TelemetryKey<T> key, T value, long timestamp) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public TelemetryKey<T> getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getKeyName() {
|
||||
return key.getName();
|
||||
}
|
||||
|
||||
public Class<T> getType() {
|
||||
return key.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return key.getName() + ": " + value + " (timestamp: " + timestamp + ")";
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue