添加IOT接口
This commit is contained in:
parent
34f86b6329
commit
9fd9602718
5
pom.xml
5
pom.xml
|
|
@ -17,6 +17,11 @@
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.thingsboard</groupId>
|
||||||
|
<artifactId>rest-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Tuoheng Device API -->
|
<!-- Tuoheng Device API -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.ruoyi</groupId>
|
<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