添加IOT接口

This commit is contained in:
孙小云 2026-01-16 09:06:30 +08:00
parent 34f86b6329
commit 9fd9602718
14 changed files with 1020 additions and 0 deletions

View File

@ -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>

View File

@ -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);
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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() {
// 这个方法的存在是为了触发类的静态初始化
// 当调用此方法时所有静态字段都会被初始化
}
}

View File

@ -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 + '\'' +
'}';
}
}

View File

@ -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;
}
};
}
}

View File

@ -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() {
// 这个方法的存在是为了触发类的静态初始化
// 当调用此方法时所有静态字段都会被初始化
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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 + ")";
}
}