From 9fd96027189a5df7540a576634bf0397c5b84c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E5=B0=8F=E4=BA=91?= Date: Fri, 16 Jan 2026 09:06:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0IOT=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 5 + .../device/domain/api/IThingsBoardDomain.java | 40 +++++ .../domain/impl/ThingsBoardDomainImpl.java | 149 ++++++++++++++++++ .../model/thingsboard/AttributeKey.java | 119 ++++++++++++++ .../model/thingsboard/AttributeMap.java | 76 +++++++++ .../model/thingsboard/AttributeValue.java | 38 +++++ .../model/thingsboard/DeviceAttributes.java | 94 +++++++++++ .../domain/model/thingsboard/DeviceInfo.java | 45 ++++++ .../model/thingsboard/DeviceIterator.java | 68 ++++++++ .../model/thingsboard/DeviceTelemetry.java | 60 +++++++ .../model/thingsboard/RestClientManager.java | 97 ++++++++++++ .../model/thingsboard/TelemetryKey.java | 119 ++++++++++++++ .../model/thingsboard/TelemetryMap.java | 66 ++++++++ .../model/thingsboard/TelemetryValue.java | 44 ++++++ 14 files changed, 1020 insertions(+) create mode 100644 src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java create mode 100644 src/main/java/com/ruoyi/device/domain/impl/ThingsBoardDomainImpl.java create mode 100644 src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeKey.java create mode 100644 src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeMap.java create mode 100644 src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeValue.java create mode 100644 src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceAttributes.java create mode 100644 src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceInfo.java create mode 100644 src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceIterator.java create mode 100644 src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceTelemetry.java create mode 100644 src/main/java/com/ruoyi/device/domain/model/thingsboard/RestClientManager.java create mode 100644 src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryKey.java create mode 100644 src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryMap.java create mode 100644 src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryValue.java diff --git a/pom.xml b/pom.xml index 189db52..fabb165 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,11 @@ + + org.thingsboard + rest-client + + com.ruoyi diff --git a/src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java b/src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java new file mode 100644 index 0000000..528b406 --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java @@ -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> getAllDevices(); + + /** + * 根据设备ID获取设备的所有属性 + * 只返回已注册的属性键对应的数据,未注册的键会被忽略 + * + * @param deviceId 设备ID + * @return 类型安全的属性映射 + */ + AttributeMap getDeviceAttributes(String deviceId); + + /** + * 根据设备ID获取设备的所有遥测数据 + * 只返回已注册的遥测键对应的数据,未注册的键会被忽略 + * + * @param deviceId 设备ID + * @return 类型安全的遥测数据映射 + */ + TelemetryMap getDeviceTelemetry(String deviceId); +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/impl/ThingsBoardDomainImpl.java b/src/main/java/com/ruoyi/device/domain/impl/ThingsBoardDomainImpl.java new file mode 100644 index 0000000..f8e32d0 --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/impl/ThingsBoardDomainImpl.java @@ -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> 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 attributeKeys = client.getAttributeKeys(id); + if (attributeKeys == null || attributeKeys.isEmpty()) { + log.debug("设备 {} 没有属性", deviceId); + return attributeMap; + } + + // 获取属性值 + List 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 timeseriesKeys = client.getTimeseriesKeys(id); + if (timeseriesKeys == null || timeseriesKeys.isEmpty()) { + log.debug("设备 {} 没有遥测数据", deviceId); + return telemetryMap; + } + + // 获取最新的遥测数据 + List 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) key).parse(value); + attributeMap.put((AttributeKey) 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) key).parse(value); + telemetryMap.put((TelemetryKey) 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()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeKey.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeKey.java new file mode 100644 index 0000000..9848c27 --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeKey.java @@ -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 属性值的类型 + */ +public class AttributeKey { + private final String name; + private final Class type; + private final ValueParser parser; + + private static final Map> REGISTRY = new HashMap<>(); + + private AttributeKey(String name, Class type, ValueParser parser) { + this.name = name; + this.type = type; + this.parser = parser; + } + + /** + * 创建属性键并注册 + */ + public static AttributeKey of(String name, Class type, ValueParser parser) { + AttributeKey key = new AttributeKey<>(name, type, parser); + REGISTRY.put(name, key); + return key; + } + + /** + * 根据名称查找已注册的属性键 + */ + public static Optional> 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 getType() { + return type; + } + + @Override + public String toString() { + return "AttributeKey{" + + "name='" + name + '\'' + + ", type=" + type.getSimpleName() + + '}'; + } + + /** + * 根据值自动推断类型并创建属性键 + */ + @SuppressWarnings("unchecked") + private static AttributeKey inferKeyFromValue(String name, Object value) { + if (value == null) { + // 默认为 String 类型 + return (AttributeKey) of(name, String.class, v -> v != null ? v.toString() : null); + } + + // 根据值的实际类型推断 + if (value instanceof Boolean) { + return (AttributeKey) 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) 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) 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) of(name, String.class, v -> v != null ? v.toString() : null); + } + } + + /** + * 值解析器接口 + */ + @FunctionalInterface + public interface ValueParser { + T parse(Object rawValue); + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeMap.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeMap.java new file mode 100644 index 0000000..f0a41ee --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeMap.java @@ -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, Object> data = new HashMap<>(); + + /** + * 添加属性值(类型安全) + */ + public void put(AttributeKey key, T value) { + data.put(key, value); + } + + /** + * 获取属性值(类型安全) + * 返回Optional避免null问题 + */ + @SuppressWarnings("unchecked") + public Optional get(AttributeKey key) { + return Optional.ofNullable((T) data.get(key)); + } + + /** + * 获取属性值,如果不存在返回默认值 + */ + @SuppressWarnings("unchecked") + public T getOrDefault(AttributeKey key, T defaultValue) { + return (T) data.getOrDefault(key, defaultValue); + } + + /** + * 检查是否包含某个键 + */ + public boolean containsKey(AttributeKey key) { + return data.containsKey(key); + } + + /** + * 获取所有键 + */ + public Set> 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeValue.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeValue.java new file mode 100644 index 0000000..4496515 --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeValue.java @@ -0,0 +1,38 @@ +package com.ruoyi.device.domain.model.thingsboard; + +/** + * 类型安全的属性值包装类 + * 避免使用Object作为返回值 + * + * @param 值的类型 + */ +public class AttributeValue { + private final AttributeKey key; + private final T value; + + public AttributeValue(AttributeKey key, T value) { + this.key = key; + this.value = value; + } + + public AttributeKey getKey() { + return key; + } + + public T getValue() { + return value; + } + + public String getKeyName() { + return key.getName(); + } + + public Class getType() { + return key.getType(); + } + + @Override + public String toString() { + return key.getName() + ": " + value; + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceAttributes.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceAttributes.java new file mode 100644 index 0000000..f30e3db --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceAttributes.java @@ -0,0 +1,94 @@ +package com.ruoyi.device.domain.model.thingsboard; + +/** + * 预定义的设备属性键 + * 使用类型安全的方式定义常用属性 + */ +public class DeviceAttributes { + + // 连接器类型 - String + public static final AttributeKey CONNECTOR_TYPE = AttributeKey.of( + "connectorType", + String.class, + value -> value != null ? value.toString() : null + ); + + // 连接器名称 - String + public static final AttributeKey CONNECTOR_NAME = AttributeKey.of( + "connectorName", + String.class, + value -> value != null ? value.toString() : null + ); + + // 网关 - String + public static final AttributeKey GATEWAY = AttributeKey.of( + "gateway", + String.class, + value -> value != null ? value.toString() : null + ); + + // 最后连接时间 - Long + public static final AttributeKey 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 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 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 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() { + // 这个方法的存在是为了触发类的静态初始化 + // 当调用此方法时,所有静态字段都会被初始化 + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceInfo.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceInfo.java new file mode 100644 index 0000000..5a2ce06 --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceInfo.java @@ -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 + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceIterator.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceIterator.java new file mode 100644 index 0000000..ed77903 --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceIterator.java @@ -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> { + + private final RestClient client; + private final int pageSize; + + public DeviceIterator(RestClient client, int pageSize) { + this.client = client; + this.pageSize = pageSize; + } + + @Override + public Iterator> iterator() { + return new Iterator>() { + private PageLink pageLink = new PageLink(pageSize); + private PageData currentPage = null; + private boolean hasNextPage = true; + + @Override + public boolean hasNext() { + return hasNextPage; + } + + @Override + public List next() { + if (!hasNext()) { + throw new NoSuchElementException("No more devices"); + } + + // 获取当前页数据 + currentPage = client.getTenantDevices("", pageLink); + + // 转换为DeviceInfo列表 + List 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; + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceTelemetry.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceTelemetry.java new file mode 100644 index 0000000..f071584 --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/DeviceTelemetry.java @@ -0,0 +1,60 @@ +package com.ruoyi.device.domain.model.thingsboard; + +/** + * 预定义的设备遥测数据键 + * 使用类型安全的方式定义常用遥测数据 + */ +public class DeviceTelemetry { + + // 空调状态 - Integer + public static final TelemetryKey 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 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 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() { + // 这个方法的存在是为了触发类的静态初始化 + // 当调用此方法时,所有静态字段都会被初始化 + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/RestClientManager.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/RestClientManager.java new file mode 100644 index 0000000..e19a686 --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/RestClientManager.java @@ -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; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryKey.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryKey.java new file mode 100644 index 0000000..c0ede6b --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryKey.java @@ -0,0 +1,119 @@ +package com.ruoyi.device.domain.model.thingsboard; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * 类型安全的遥测数据键 + * 包含时间戳信息 + * + * @param 遥测值的类型 + */ +public class TelemetryKey { + private final String name; + private final Class type; + private final ValueParser parser; + + private static final Map> REGISTRY = new HashMap<>(); + + private TelemetryKey(String name, Class type, ValueParser parser) { + this.name = name; + this.type = type; + this.parser = parser; + } + + /** + * 创建遥测键并注册 + */ + public static TelemetryKey of(String name, Class type, ValueParser parser) { + TelemetryKey key = new TelemetryKey<>(name, type, parser); + REGISTRY.put(name, key); + return key; + } + + /** + * 根据名称查找已注册的遥测键 + */ + public static Optional> 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 getType() { + return type; + } + + @Override + public String toString() { + return "TelemetryKey{" + + "name='" + name + '\'' + + ", type=" + type.getSimpleName() + + '}'; + } + + /** + * 根据值自动推断类型并创建遥测键 + */ + @SuppressWarnings("unchecked") + private static TelemetryKey inferKeyFromValue(String name, Object value) { + if (value == null) { + // 默认为 String 类型 + return (TelemetryKey) of(name, String.class, v -> v != null ? v.toString() : null); + } + + // 根据值的实际类型推断 + if (value instanceof Boolean) { + return (TelemetryKey) 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) 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) 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) of(name, String.class, v -> v != null ? v.toString() : null); + } + } + + /** + * 值解析器接口 + */ + @FunctionalInterface + public interface ValueParser { + T parse(Object rawValue); + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryMap.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryMap.java new file mode 100644 index 0000000..bcea02c --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryMap.java @@ -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, TelemetryValue> data = new HashMap<>(); + + /** + * 添加遥测值(类型安全) + */ + public void put(TelemetryKey key, T value, long timestamp) { + data.put(key, new TelemetryValue<>(key, value, timestamp)); + } + + /** + * 获取遥测值(类型安全) + */ + @SuppressWarnings("unchecked") + public Optional> get(TelemetryKey key) { + return Optional.ofNullable((TelemetryValue) data.get(key)); + } + + /** + * 检查是否包含某个键 + */ + public boolean containsKey(TelemetryKey key) { + return data.containsKey(key); + } + + /** + * 获取所有键 + */ + public Set> 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryValue.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryValue.java new file mode 100644 index 0000000..9ec278b --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryValue.java @@ -0,0 +1,44 @@ +package com.ruoyi.device.domain.model.thingsboard; + +/** + * 类型安全的遥测数据值包装类 + * 包含时间戳信息 + * + * @param 值的类型 + */ +public class TelemetryValue { + private final TelemetryKey key; + private final T value; + private final long timestamp; + + public TelemetryValue(TelemetryKey key, T value, long timestamp) { + this.key = key; + this.value = value; + this.timestamp = timestamp; + } + + public TelemetryKey getKey() { + return key; + } + + public T getValue() { + return value; + } + + public long getTimestamp() { + return timestamp; + } + + public String getKeyName() { + return key.getName(); + } + + public Class getType() { + return key.getType(); + } + + @Override + public String toString() { + return key.getName() + ": " + value + " (timestamp: " + timestamp + ")"; + } +} \ No newline at end of file