diff --git a/src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java b/src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java index 528b406..b61ad0a 100644 --- a/src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java +++ b/src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java @@ -37,4 +37,22 @@ public interface IThingsBoardDomain { * @return 类型安全的遥测数据映射 */ TelemetryMap getDeviceTelemetry(String deviceId); + + /** + * 根据设备ID获取设备的预定义属性 + * 只返回在 DeviceAttributes 中预定义的属性 + * + * @param deviceId 设备ID + * @return 类型安全的属性映射,只包含预定义的属性 + */ + AttributeMap getPredefinedDeviceAttributes(String deviceId); + + /** + * 根据设备ID获取设备的预定义遥测数据 + * 只返回在 DeviceTelemetry 中预定义的遥测数据 + * + * @param deviceId 设备ID + * @return 类型安全的遥测数据映射,只包含预定义的遥测数据 + */ + TelemetryMap getPredefinedDeviceTelemetry(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 index 25458a3..73faf6e 100644 --- a/src/main/java/com/ruoyi/device/domain/impl/ThingsBoardDomainImpl.java +++ b/src/main/java/com/ruoyi/device/domain/impl/ThingsBoardDomainImpl.java @@ -4,6 +4,7 @@ package com.ruoyi.device.domain.impl; import com.ruoyi.device.domain.api.IThingsBoardDomain; import com.ruoyi.device.domain.model.thingsboard.*; import com.ruoyi.device.domain.model.thingsboard.constants.DeviceAttributes; +import com.ruoyi.device.domain.model.thingsboard.constants.DeviceTelemetry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -113,6 +114,65 @@ public class ThingsBoardDomainImpl implements IThingsBoardDomain { return telemetryMap; } + + @Override + public AttributeMap getPredefinedDeviceAttributes(String deviceId) { + // 先获取所有属性(已经处理了异常情况) + AttributeMap allAttributes = getDeviceAttributes(deviceId); + + // 创建新的 AttributeMap 只包含预定义的键 + AttributeMap predefinedAttributes = new AttributeMap(); + + // 获取预定义的键名称集合 + List predefinedKeyNames = DeviceAttributes.getPredefinedKeys() + .stream() + .map(AttributeKey::getName) + .toList(); + + // 过滤:只保留预定义的键 + for (AttributeKey key : allAttributes.keySet()) { + if (predefinedKeyNames.contains(key.getName())) { + // 复制到新的 map + allAttributes.get(key).ifPresent(value -> { + @SuppressWarnings("unchecked") + AttributeKey objKey = (AttributeKey) key; + predefinedAttributes.put(objKey, value); + }); + } + } + + return predefinedAttributes; + } + + @Override + public TelemetryMap getPredefinedDeviceTelemetry(String deviceId) { + // 先获取所有遥测数据(已经处理了 null 值问题) + TelemetryMap allTelemetry = getDeviceTelemetry(deviceId); + + // 创建新的 TelemetryMap 只包含预定义的键 + TelemetryMap predefinedTelemetry = new TelemetryMap(); + + // 获取预定义的键名称集合 + List predefinedKeyNames = DeviceTelemetry.getPredefinedKeys() + .stream() + .map(TelemetryKey::getName) + .toList(); + + // 过滤:只保留预定义的键 + for (TelemetryKey key : allTelemetry.keySet()) { + if (predefinedKeyNames.contains(key.getName())) { + // 复制到新的 map + allTelemetry.get(key).ifPresent(telemetryValue -> { + @SuppressWarnings("unchecked") + TelemetryKey objKey = (TelemetryKey) key; + predefinedTelemetry.put(objKey, telemetryValue.getValue(), telemetryValue.getTimestamp()); + }); + } + } + + return predefinedTelemetry; + } + /** * 解析属性并添加到AttributeMap * 使用延迟注册机制,自动处理所有属性 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 index 9848c27..449df77 100644 --- a/src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeKey.java +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/AttributeKey.java @@ -1,8 +1,7 @@ package com.ruoyi.device.domain.model.thingsboard; - -import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; /** * 类型安全的属性键 @@ -15,7 +14,7 @@ public class AttributeKey { private final Class type; private final ValueParser parser; - private static final Map> REGISTRY = new HashMap<>(); + private static final Map> REGISTRY = new ConcurrentHashMap<>(); private AttributeKey(String name, Class type, ValueParser parser) { this.name = name; @@ -51,6 +50,15 @@ public class AttributeKey { return REGISTRY.computeIfAbsent(name, k -> inferKeyFromValue(name, value)); } + /** + * 获取所有已注册的属性键集合 + * + * @return 所有已注册的属性键 + */ + public static Map> getAllRegisteredKeys() { + return new ConcurrentHashMap<>(REGISTRY); + } + /** * 解析原始值为目标类型 */ @@ -76,36 +84,37 @@ public class AttributeKey { /** * 根据值自动推断类型并创建属性键 + * 注意:这个方法不能调用 of() 方法,因为会导致递归更新 */ @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); + return (AttributeKey) new AttributeKey<>(name, String.class, v -> v != null ? v.toString() : null); } // 根据值的实际类型推断 if (value instanceof Boolean) { - return (AttributeKey) of(name, Boolean.class, v -> { + return (AttributeKey) new AttributeKey<>(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 -> { + return (AttributeKey) new AttributeKey<>(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 -> { + return (AttributeKey) new AttributeKey<>(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); + return (AttributeKey) new AttributeKey<>(name, String.class, v -> v != null ? v.toString() : null); } } 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 index c0ede6b..9c798f8 100644 --- a/src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryKey.java +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/TelemetryKey.java @@ -1,8 +1,12 @@ package com.ruoyi.device.domain.model.thingsboard; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.device.domain.model.thingsboard.attributes.psdk.PsdkDevice; -import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; /** * 类型安全的遥测数据键 @@ -15,7 +19,7 @@ public class TelemetryKey { private final Class type; private final ValueParser parser; - private static final Map> REGISTRY = new HashMap<>(); + private static final Map> REGISTRY = new ConcurrentHashMap<>(); private TelemetryKey(String name, Class type, ValueParser parser) { this.name = name; @@ -51,6 +55,15 @@ public class TelemetryKey { return REGISTRY.computeIfAbsent(name, k -> inferKeyFromValue(name, value)); } + /** + * 获取所有已注册的遥测键集合 + * + * @return 所有已注册的遥测键 + */ + public static Map> getAllRegisteredKeys() { + return new ConcurrentHashMap<>(REGISTRY); + } + /** * 解析原始值为目标类型 */ @@ -76,39 +89,100 @@ public class TelemetryKey { /** * 根据值自动推断类型并创建遥测键 + * 注意:这个方法不能调用 of() 方法,因为会导致递归更新 */ @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); + return (TelemetryKey) new TelemetryKey<>(name, String.class, v -> v != null ? v.toString() : null); + } + + // 特殊处理:psdk_widget_values 字段 + if ("psdk_widget_values".equals(name)) { + return (TelemetryKey) createPsdkWidgetValuesKeyWithoutRegistering(); } // 根据值的实际类型推断 if (value instanceof Boolean) { - return (TelemetryKey) of(name, Boolean.class, v -> { + return (TelemetryKey) new TelemetryKey<>(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 -> { + return (TelemetryKey) new TelemetryKey<>(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 -> { + return (TelemetryKey) new TelemetryKey<>(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); + return (TelemetryKey) new TelemetryKey<>(name, String.class, v -> v != null ? v.toString() : null); } } + /** + * 创建 psdk_widget_values 的遥测键(不注册到 REGISTRY) + * 用于解析 Python 风格的字典字符串为 List + * 注意:不能调用 of() 方法,避免递归更新 + */ + @SuppressWarnings("unchecked") + private static TelemetryKey> createPsdkWidgetValuesKeyWithoutRegistering() { + ObjectMapper objectMapper = new ObjectMapper(); + // 配置 ObjectMapper 忽略未知字段 + objectMapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + return new TelemetryKey<>( + "psdk_widget_values", + (Class>) (Class) List.class, + value -> { + if (value == null) return null; + + try { + // 如果已经是 List 类型,直接返回 + if (value instanceof List) { + return (List) value; + } + + // 如果是字符串,需要处理 Python 风格的字典格式 + if (value instanceof String) { + String jsonStr = (String) value; + + // 将 Python 风格的字典转换为标准 JSON 格式 + // 1. 将单引号替换为双引号 + jsonStr = jsonStr.replace("'", "\""); + + // 2. 处理 True/False/None (如果有的话) + jsonStr = jsonStr.replace(": True", ": true") + .replace(": False", ": false") + .replace(": None", ": null"); + + return objectMapper.readValue( + jsonStr, + new TypeReference>() {} + ); + } + + // 如果是其他对象(如 JsonNode),转换为 JSON 再解析 + String json = objectMapper.writeValueAsString(value); + return objectMapper.readValue( + json, + new TypeReference>() {} + ); + } catch (Exception e) { + throw new RuntimeException("Failed to parse psdk_widget_values: " + e.getMessage(), e); + } + } + ); + } + /** * 值解析器接口 */ diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/attributes/psdk/PsdkDevice.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/attributes/psdk/PsdkDevice.java new file mode 100644 index 0000000..e6a6c10 --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/attributes/psdk/PsdkDevice.java @@ -0,0 +1,90 @@ +package com.ruoyi.device.domain.model.thingsboard.attributes.psdk; + +import java.util.List; + +/** + * PSDK + */ +public class PsdkDevice { + private int psdk_index; + private String psdk_lib_version; + private String psdk_name; + private String psdk_sn; + private String psdk_version; + private Speaker speaker; + private List values; + + // 构造方法 + public PsdkDevice() { + } + + // Getter和Setter方法 + public int getPsdk_index() { + return psdk_index; + } + + public void setPsdk_index(int psdk_index) { + this.psdk_index = psdk_index; + } + + public String getPsdk_lib_version() { + return psdk_lib_version; + } + + public void setPsdk_lib_version(String psdk_lib_version) { + this.psdk_lib_version = psdk_lib_version; + } + + public String getPsdk_name() { + return psdk_name; + } + + public void setPsdk_name(String psdk_name) { + this.psdk_name = psdk_name; + } + + public String getPsdk_sn() { + return psdk_sn; + } + + public void setPsdk_sn(String psdk_sn) { + this.psdk_sn = psdk_sn; + } + + public String getPsdk_version() { + return psdk_version; + } + + public void setPsdk_version(String psdk_version) { + this.psdk_version = psdk_version; + } + + public Speaker getSpeaker() { + return speaker; + } + + public void setSpeaker(Speaker speaker) { + this.speaker = speaker; + } + + public List getValues() { + return values; + } + + public void setValues(List values) { + this.values = values; + } + + @Override + public String toString() { + return "PsdkDevice{" + + "psdk_index=" + psdk_index + + ", psdk_lib_version='" + psdk_lib_version + '\'' + + ", psdk_name='" + psdk_name + '\'' + + ", psdk_sn='" + psdk_sn + '\'' + + ", psdk_version='" + psdk_version + '\'' + + ", speaker=" + speaker + + ", values=" + values + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/attributes/psdk/Speaker.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/attributes/psdk/Speaker.java new file mode 100644 index 0000000..0103b4d --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/attributes/psdk/Speaker.java @@ -0,0 +1,78 @@ +package com.ruoyi.device.domain.model.thingsboard.attributes.psdk; + +/** + * Speaker + */ +public class Speaker { + private String play_file_md5; + private String play_file_name; + private int play_mode; + private int play_volume; + private int system_state; + private int work_mode; + + // 构造方法 + public Speaker() { + } + + // Getter和Setter方法 + public String getPlay_file_md5() { + return play_file_md5; + } + + public void setPlay_file_md5(String play_file_md5) { + this.play_file_md5 = play_file_md5; + } + + public String getPlay_file_name() { + return play_file_name; + } + + public void setPlay_file_name(String play_file_name) { + this.play_file_name = play_file_name; + } + + public int getPlay_mode() { + return play_mode; + } + + public void setPlay_mode(int play_mode) { + this.play_mode = play_mode; + } + + public int getPlay_volume() { + return play_volume; + } + + public void setPlay_volume(int play_volume) { + this.play_volume = play_volume; + } + + public int getSystem_state() { + return system_state; + } + + public void setSystem_state(int system_state) { + this.system_state = system_state; + } + + public int getWork_mode() { + return work_mode; + } + + public void setWork_mode(int work_mode) { + this.work_mode = work_mode; + } + + @Override + public String toString() { + return "Speaker{" + + "play_file_md5='" + play_file_md5 + '\'' + + ", play_file_name='" + play_file_name + '\'' + + ", play_mode=" + play_mode + + ", play_volume=" + play_volume + + ", system_state=" + system_state + + ", work_mode=" + work_mode + + '}'; + } +} diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/attributes/psdk/ValueItem.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/attributes/psdk/ValueItem.java new file mode 100644 index 0000000..a7355f3 --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/attributes/psdk/ValueItem.java @@ -0,0 +1,43 @@ +package com.ruoyi.device.domain.model.thingsboard.attributes.psdk; + +/** + * ValueItem + */ +public class ValueItem { + private int index; + private int value; + + // 构造方法 + public ValueItem() { + } + + public ValueItem(int index, int value) { + this.index = index; + this.value = value; + } + + // Getter和Setter方法 + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + @Override + public String toString() { + return "ValueItem{" + + "index=" + index + + ", value=" + value + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/constants/DeviceAttributes.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/constants/DeviceAttributes.java index 0dc2ce2..9024138 100644 --- a/src/main/java/com/ruoyi/device/domain/model/thingsboard/constants/DeviceAttributes.java +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/constants/DeviceAttributes.java @@ -2,6 +2,9 @@ package com.ruoyi.device.domain.model.thingsboard.constants; import com.ruoyi.device.domain.model.thingsboard.AttributeKey; +import java.util.Arrays; +import java.util.List; + /** * 预定义的设备属性键 * 使用类型安全的方式定义常用属性 @@ -22,20 +25,13 @@ public class DeviceAttributes { value -> value != null ? value.toString() : null ); - // 网关名称 - String + // 网关 - String public static final AttributeKey GATEWAY = AttributeKey.of( "gateway", String.class, value -> value != null ? value.toString() : null ); - // 机场SN - String - public static final AttributeKey DOCK_SN = AttributeKey.of( - "dock_sn", - String.class, - value -> value != null ? value.toString() : null - ); - // 最后连接时间 - Long public static final AttributeKey LAST_CONNECT_TIME = AttributeKey.of( "lastConnectTime", @@ -92,6 +88,23 @@ public class DeviceAttributes { // 工具类,禁止实例化 } + /** + * 获取所有预定义的属性键 + * + * @return 预定义的属性键列表 + */ + public static List> getPredefinedKeys() { + return Arrays.asList( + CONNECTOR_TYPE, + CONNECTOR_NAME, + GATEWAY, + LAST_CONNECT_TIME, + ACTIVE, + LAST_ACTIVITY_TIME, + LAST_DISCONNECT_TIME + ); + } + /** * 初始化所有属性键 * 确保所有静态字段被加载和注册 diff --git a/src/main/java/com/ruoyi/device/domain/model/thingsboard/constants/DeviceTelemetry.java b/src/main/java/com/ruoyi/device/domain/model/thingsboard/constants/DeviceTelemetry.java index a0efb93..0f5278f 100644 --- a/src/main/java/com/ruoyi/device/domain/model/thingsboard/constants/DeviceTelemetry.java +++ b/src/main/java/com/ruoyi/device/domain/model/thingsboard/constants/DeviceTelemetry.java @@ -1,6 +1,13 @@ package com.ruoyi.device.domain.model.thingsboard.constants; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.ruoyi.device.domain.model.thingsboard.TelemetryKey; +import com.ruoyi.device.domain.model.thingsboard.attributes.psdk.PsdkDevice; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** * 预定义的设备遥测数据键 @@ -8,6 +15,10 @@ import com.ruoyi.device.domain.model.thingsboard.TelemetryKey; */ public class DeviceTelemetry { + // Jackson ObjectMapper 用于 JSON 解析 + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + // 空调状态 - Integer public static final TelemetryKey AIR_CONDITIONER_STATE = TelemetryKey.of( "air_conditioner.air_conditioner_state", @@ -47,10 +58,69 @@ public class DeviceTelemetry { } ); + // PSDK Widget Values - List + @SuppressWarnings("unchecked") + public static final TelemetryKey> PSDK_WIDGET_VALUES = TelemetryKey.of( + "psdk_widget_values", + (Class>) (Class) List.class, + value -> { + if (value == null) return null; + + try { + // 如果已经是 List 类型,直接返回 + if (value instanceof List) { + return (List) value; + } + + // 如果是字符串,需要处理 Python 风格的字典格式 + if (value instanceof String) { + String jsonStr = (String) value; + + // 将 Python 风格的字典转换为标准 JSON 格式 + // 1. 将单引号替换为双引号 + jsonStr = jsonStr.replace("'", "\""); + + // 2. 处理 True/False/None (如果有的话) + jsonStr = jsonStr.replace(": True", ": true") + .replace(": False", ": false") + .replace(": None", ": null"); + + return OBJECT_MAPPER.readValue( + jsonStr, + new TypeReference>() {} + ); + } + + // 如果是其他对象(如 JsonNode),转换为 JSON 再解析 + String json = OBJECT_MAPPER.writeValueAsString(value); + return OBJECT_MAPPER.readValue( + json, + new TypeReference>() {} + ); + } catch (Exception e) { + throw new RuntimeException("Failed to parse psdk_widget_values: " + e.getMessage(), e); + } + } + ); + private DeviceTelemetry() { // 工具类,禁止实例化 } + /** + * 获取所有预定义的遥测键 + * + * @return 预定义的遥测键列表 + */ + public static List> getPredefinedKeys() { + return Arrays.asList( + AIR_CONDITIONER_STATE, + TEMPERATURE, + HUMIDITY, + PSDK_WIDGET_VALUES + ); + } + /** * 初始化所有遥测键 * 确保所有静态字段被加载和注册