diff --git a/modules/JiShe.CollectBus.IoTDB/Exceptions/IoTException.cs b/modules/JiShe.CollectBus.IoTDB/Exceptions/IoTException.cs new file mode 100644 index 0000000..93bed4f --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Exceptions/IoTException.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.IoTDB.Exceptions +{ + /// + /// IoTDB异常 + /// + public class IoTException : Exception + { + public int ErrorCode { get; } + + public IoTException(string message, int errorCode) + : base($"{message} (Code: {errorCode})") + { + ErrorCode = errorCode; + } + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/JiShe.CollectBus.IoTDB.csproj b/modules/JiShe.CollectBus.IoTDB/JiShe.CollectBus.IoTDB.csproj index 3911399..78b81e3 100644 --- a/modules/JiShe.CollectBus.IoTDB/JiShe.CollectBus.IoTDB.csproj +++ b/modules/JiShe.CollectBus.IoTDB/JiShe.CollectBus.IoTDB.csproj @@ -14,7 +14,6 @@ - + diff --git a/modules/JiShe.CollectBus.IoTDB/Provider/IoTDBProvider.cs b/modules/JiShe.CollectBus.IoTDB/Provider/IoTDBProvider.cs index bacbf61..db86791 100644 --- a/modules/JiShe.CollectBus.IoTDB/Provider/IoTDBProvider.cs +++ b/modules/JiShe.CollectBus.IoTDB/Provider/IoTDBProvider.cs @@ -21,6 +21,7 @@ using Microsoft.Extensions.Logging; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; using JiShe.CollectBus.Analyzers.Shared; +using JiShe.CollectBus.IoTDB.Exceptions; namespace JiShe.CollectBus.IoTDB.Provider { @@ -250,128 +251,337 @@ namespace JiShe.CollectBus.IoTDB.Provider } } + ///// + ///// 构建Tablet + ///// + ///// + ///// 表实体 + ///// 设备元数据 + ///// + //private Tablet BuildTablet(IEnumerable entities, DeviceMetadata metadata) where T : IoTEntity + //{ + // var timestamps = new List(); + // var values = new List>(); + // var devicePaths = new HashSet(); + // List tempColumnNames = new List(); + // tempColumnNames.AddRange(metadata.ColumnNames); + + // var accessor = SourceEntityAccessorFactory.GetAccessor(); + + // var memberCache = new Dictionary(); // 缓存优化查询 + // // 预构建成员缓存(Key: NameOrPath) + // foreach (var member in accessor.MemberList) + // { + // memberCache[member.NameOrPath] = member; + // } + + // if (accessor.EntityType == null || metadata.EntityType == null) + // { + // throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 {nameof(T)}的EntityType 没有指定,属于异常情况,-101"); + // } + + // if (metadata.EntityType != accessor.EntityType) + // { + // throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 {nameof(T)}的EntityType 和{nameof(DeviceMetadata)}的 EntityType 不一致,属于异常情况,-102"); + // } + + // if (metadata.EntityType == EntityTypeEnum.TreeModel && _runtimeContext.UseTableSessionPool == true) + // { + // throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 tree模型不能使用table模型Session连接,属于异常情况,-103"); + // } + // else if (metadata.EntityType == EntityTypeEnum.TableModel && _runtimeContext.UseTableSessionPool == false) + // { + // throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 table模型不能使用tree模型Session连接,属于异常情况,-104"); + // } + + // string tableNameOrTreePath = string.Empty; + // var tableNameOrTreePathAttribute = typeof(T).GetCustomAttribute(); + // if (tableNameOrTreePathAttribute != null) + // { + // tableNameOrTreePath = tableNameOrTreePathAttribute.TableNameOrTreePath; + // } + + // foreach (var entity in entities) + // { + // timestamps.Add(entity.Timestamps); + // var rowValues = new List(); + + // foreach (var measurement in metadata.ColumnNames) + // { + // if (!memberCache.TryGetValue(measurement, out var member)) + // { + // throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构时{accessor.EntityName}没有找到{measurement}对应的member信息,-105"); + // } + + // var value = member.GetValue(entity); + + // // 特性查询优化 + // var attributes = member.CustomAttributes ?? Enumerable.Empty(); + // var singleMeasuringAttr = attributes.OfType().FirstOrDefault(); + // if (singleMeasuringAttr != null)//如果是单侧点 + // { + + // var tupleItem1Key = $"{member.NameOrPath}.Item1"; + // if (!memberCache.TryGetValue(tupleItem1Key, out var tuple1Member)) + // { + // throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构时{accessor.EntityName} 没有找到{measurement}对应的member Item1 信息,-106"); + // } + // int indexOf = metadata.ColumnNames.IndexOf(measurement); + // tempColumnNames[indexOf] = (string)tuple1Member.GetValue(entity); + + // var tupleItem2Key = $"{member.NameOrPath}.Item2"; + // if (!memberCache.TryGetValue(tupleItem2Key, out var tuple2Member)) + // { + // throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构时{accessor.EntityName} 没有找到{measurement}对应的member Item2 信息,-107"); + // } + + // value = tuple2Member.GetValue(entity); + // } + + // if (value != null) + // { + // var tempValue = member.DeclaredTypeName.ToUpper() switch + // { + // "DATETIME" => Convert.ToDateTime(value).GetDateTimeOffset().ToUnixTimeNanoseconds(), + // _ => value + // }; + + // rowValues.Add(tempValue); + // } + // else + // { + // rowValues.Add(value); + // } + // } + + // values.Add(rowValues); + + // //如果指定了路径 + // if (!string.IsNullOrWhiteSpace(tableNameOrTreePath)) + // { + // devicePaths.Add(tableNameOrTreePath); + // } + // else + // { + // if (!_runtimeContext.UseTableSessionPool)//树模型 + // { + // devicePaths.Add(DevicePathBuilder.GetDevicePath(entity)); + // } + // else + // { + // devicePaths.Add(DevicePathBuilder.GetTableName()); + // } + // } + // } + + // if (devicePaths.Count > 1) + // { + // throw new Exception($"{nameof(BuildTablet)} 构建Tablet《{typeof(T).Name}》时,批量插入的设备路径不一致。"); + // } + + // return _runtimeContext.UseTableSessionPool + // ? BuildTableSessionTablet(metadata, devicePaths.First(), tempColumnNames, values, timestamps) + // : BuildSessionTablet(metadata, devicePaths.First(), tempColumnNames,values, timestamps); + //} + /// /// 构建Tablet /// /// - /// 表实体 + /// 表实体集合 /// 设备元数据 /// private Tablet BuildTablet(IEnumerable entities, DeviceMetadata metadata) where T : IoTEntity { - var timestamps = new List(); - var values = new List>(); - var devicePaths = new HashSet(); - List tempColumnNames = new List(); - tempColumnNames.AddRange(metadata.ColumnNames); - - var accessor = SourceEntityAccessorFactory.GetAccessor(); + // 前置校验 + ValidateMetadataAndAccessor(metadata, out var accessor); - var memberCache = new Dictionary(); // 缓存优化查询 - // 预构建成员缓存(Key: NameOrPath) - foreach (var member in accessor.MemberList) + // 初始化数据结构 + var (timestamps, values, devicePaths) = (new List(), new List>(), new HashSet()); + var tempColumnNames = new List(metadata.ColumnNames); + var memberCache = BuildMemberCache(accessor); + var tableNameOrTreePath = GetTableNameOrTreePath(); + + // 处理每个实体 + foreach (var entity in entities) { - memberCache[member.NameOrPath] = member; + ProcessEntity(entity, accessor, metadata, memberCache, tempColumnNames, timestamps, values); + UpdateDevicePaths(entity, tableNameOrTreePath, devicePaths); } + // 后置校验与返回 + ValidateDevicePaths(devicePaths); + // return CreateFinalTablet(metadata, devicePaths.First(), tempColumnNames, values, timestamps); + return _runtimeContext.UseTableSessionPool + ? BuildTableSessionTablet(metadata, devicePaths.First(), tempColumnNames, values, timestamps) + : BuildSessionTablet(metadata, devicePaths.First(), tempColumnNames, values, timestamps); + } + + private void ValidateMetadataAndAccessor(DeviceMetadata metadata, out ISourceEntityAccessor accessor) where T : IoTEntity + { + accessor = SourceEntityAccessorFactory.GetAccessor(); + if (accessor.EntityType == null || metadata.EntityType == null) { - throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 {nameof(T)}的EntityType 没有指定,属于异常情况,-101"); + throw new IoTException($"{nameof(BuildTablet)} 构建IoTDB数据结构时,EntityType未指定", -101); } if (metadata.EntityType != accessor.EntityType) { - throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 {nameof(T)}的EntityType 和{nameof(DeviceMetadata)}的 EntityType 不一致,属于异常情况,-102"); + throw new IoTException($"{nameof(BuildTablet)} 构建IoTDB数据结构时,EntityType不一致", -102); } - if (metadata.EntityType == EntityTypeEnum.TreeModel && _runtimeContext.UseTableSessionPool == true) + bool isTableModel = accessor.EntityType == EntityTypeEnum.TableModel; + if (_runtimeContext.UseTableSessionPool != isTableModel) { - throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 tree模型不能使用table模型Session连接,属于异常情况,-103"); + throw new IoTException($"{nameof(BuildTablet)} 构建IoTDB数据结构时,Session类型不匹配: 预期{(isTableModel ? "Table" : "Tree")}模型", isTableModel ? -104 : -103); } - else if (metadata.EntityType == EntityTypeEnum.TableModel && _runtimeContext.UseTableSessionPool == false) + } + + /// + /// 处理实体并获取值 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + private void ProcessEntity( + T entity, + ISourceEntityAccessor accessor, + DeviceMetadata metadata, + Dictionary memberCache, + List tempColumnNames, + List timestamps, + List> values) where T : IoTEntity + { + timestamps.Add(entity.Timestamps); + var rowValues = new object[metadata.ColumnNames.Count]; + + Parallel.ForEach(metadata.ColumnNames, (measurement, state, index) => { - throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 table模型不能使用tree模型Session连接,属于异常情况,-104"); - } - - string tableNameOrTreePath = string.Empty; - var tableNameOrTreePathAttribute = typeof(T).GetCustomAttribute(); - if (tableNameOrTreePathAttribute != null) - { - tableNameOrTreePath = tableNameOrTreePathAttribute.TableNameOrTreePath; - } - - foreach (var entity in entities) - { - timestamps.Add(entity.Timestamps); - var rowValues = new List(); - - foreach (var measurement in tempColumnNames) - { - if (!memberCache.TryGetValue(measurement, out var member)) - { - throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构时{accessor.EntityName}没有找到{measurement}对应的member信息,-105"); - } - - var value = member.GetValue(entity); - - // 特性查询优化 - var attributes = member.CustomAttributes ?? Enumerable.Empty(); - var singleMeasuringAttr = attributes.OfType().FirstOrDefault(); - if (singleMeasuringAttr != null)//如果是单侧点 - { - var tupleItemKey = $"{member.NameOrPath}.Item2"; - if (!memberCache.TryGetValue(tupleItemKey, out var tupleMember)) - { - throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构时{accessor.EntityName} 没有找到{measurement}对应的member Item2 信息,-106"); - } - - value = tupleMember.GetValue(entity); - } - - if (value != null) - { - var tempValue = member.DeclaredTypeName.ToUpper() switch - { - "DATETIME" => Convert.ToDateTime(value).GetDateTimeOffset().ToUnixTimeNanoseconds(), - _ => value - }; - - rowValues.Add(tempValue); - } - else - { - rowValues.Add(value); - } - } - - values.Add(rowValues); - - //如果指定了路径 - if (!string.IsNullOrWhiteSpace(tableNameOrTreePath)) + if (!memberCache.TryGetValue(measurement, out var member)) { - devicePaths.Add(tableNameOrTreePath); + throw new IoTException($"{nameof(BuildTablet)} 构建IoTDB数据结构时,找不到成员: {measurement}", -105); } - else + + object value = ResolveMemberValue(entity, member, memberCache, tempColumnNames, (int)index); + rowValues[index] = ConvertValueByType(member, value); + }); + + values.Add(rowValues.ToList()); + } + + private object ResolveMemberValue( + T entity, + EntityMemberInfo member, + Dictionary memberCache, + List tempColumnNames, + int columnIndex) where T : IoTEntity + { + // 单测点逻辑 + if (member.CustomAttributes?.OfType().FirstOrDefault() is { } attr) + { + var tuple1Key = $"{member.NameOrPath}.Item1"; + var tuple2Key = $"{member.NameOrPath}.Item2"; + + if (!memberCache.TryGetValue(tuple1Key, out var tuple1) || !memberCache.TryGetValue(tuple2Key, out var tuple2)) { - if (!_runtimeContext.UseTableSessionPool)//树模型 - { - devicePaths.Add(DevicePathBuilder.GetDevicePath(entity)); - } - else - { - devicePaths.Add(DevicePathBuilder.GetTableName()); - } + throw new IoTException($"{nameof(BuildTablet)} 构建IoTDB数据结构时,单侧点元组成员缺失", -106); } + + tempColumnNames[columnIndex] = (string)tuple1.GetValue(entity); + return tuple2.GetValue(entity); + } + return member.GetValue(entity); + } + + /// + /// 设置实体的成员值 + /// + /// + /// + /// + private object ConvertValueByType(EntityMemberInfo member, object value) + { + return member.DeclaredTypeName switch + { + "DATETIME" => Convert.ToDateTime(value).GetDateTimeOffset().ToUnixTimeNanoseconds(), + _ => value + }; + } + + /// + /// 处理设备路径 + /// + /// + /// + /// + /// + private void UpdateDevicePaths( + T entity, + string tableNameOrTreePath, + HashSet devicePaths) where T : IoTEntity + { + if (!string.IsNullOrEmpty(tableNameOrTreePath)) + { + devicePaths.Add(tableNameOrTreePath); + return; + } + + var path = _runtimeContext.UseTableSessionPool + ? DevicePathBuilder.GetTableName() + : DevicePathBuilder.GetDevicePath(entity); + devicePaths.Add(path); + } + + /// + /// 验证设备路径 + /// + /// + private void ValidateDevicePaths(HashSet devicePaths) + { + if (devicePaths.Count == 0) + { + throw new IoTException($"{nameof(BuildTablet)} 构建IoTDB数据结构时,设备路径集合为空", -108); } if (devicePaths.Count > 1) { - throw new Exception($"{nameof(BuildTablet)} 构建Tablet《{typeof(T).Name}》时,批量插入的设备路径不一致。"); + var paths = string.Join(", ", devicePaths.Take(3)); + { + throw new IoTException($"{nameof(BuildTablet)} 构建IoTDB数据结构时,设备路径不一致。检测到路径: {paths}...", -109); + } } + } - return _runtimeContext.UseTableSessionPool - ? BuildTableSessionTablet(metadata, devicePaths.First(), values, timestamps) - : BuildSessionTablet(metadata, devicePaths.First(), values, timestamps); + /// + /// 缓存优化:避免重复反射 + /// + /// + /// + private string GetTableNameOrTreePath() + { + return AttributeCache.TableNameOrTreePath; + } + + /// + /// 特性缓存辅助类 + /// + /// + private static class AttributeCache + { + public static readonly string TableNameOrTreePath; + + static AttributeCache() + { + var attr = typeof(T).GetCustomAttribute(); + TableNameOrTreePath = attr?.TableNameOrTreePath ?? string.Empty; + } } /// @@ -379,16 +589,17 @@ namespace JiShe.CollectBus.IoTDB.Provider /// /// 已解析的设备数据元数据 /// 设备路径 + /// 数据列集合 /// 数据集合 /// 时间戳集合 /// - private Tablet BuildSessionTablet(DeviceMetadata metadata, string devicePath, List> values, List timestamps) + private Tablet BuildSessionTablet(DeviceMetadata metadata, string devicePath, List columns, List> values, List timestamps) { //todo 树模型需要去掉TAG类型和ATTRIBUTE类型的字段,只需要保留FIELD类型字段即可 return new Tablet( devicePath, - metadata.ColumnNames, + columns, metadata.DataTypes, values, timestamps @@ -400,14 +611,15 @@ namespace JiShe.CollectBus.IoTDB.Provider /// /// 已解析的设备数据元数据 /// 表名称 + /// 数据列集合 /// 数据集合 /// 时间戳集合 /// - private Tablet BuildTableSessionTablet(DeviceMetadata metadata, string tableName, List> values, List timestamps) + private Tablet BuildTableSessionTablet(DeviceMetadata metadata, string tableName, List columns,List> values, List timestamps) { var tablet = new Tablet( tableName, - metadata.ColumnNames, + columns, metadata.ColumnCategories, metadata.DataTypes, values, @@ -535,13 +747,7 @@ namespace JiShe.CollectBus.IoTDB.Provider var metadata = await GetMetadata(); var accessor = SourceEntityAccessorFactory.GetAccessor(); - var memberCache = new Dictionary(); // 缓存优化查询 - - // 预构建成员缓存(Key: NameOrPath) - foreach (var member in accessor.MemberList) - { - memberCache[member.NameOrPath] = member; - } + var memberCache = BuildMemberCache(accessor); var columns = new List() { "Timestamps" }; var dataTypes = new List() { TSDataType.TIMESTAMP }; @@ -596,13 +802,7 @@ namespace JiShe.CollectBus.IoTDB.Provider private List CollectColumnMetadata(ISourceEntityAccessor accessor) { var columns = new List(); - var memberCache = new Dictionary(); // 缓存优化查询 - - // 预构建成员缓存(Key: NameOrPath) - foreach (var member in accessor.MemberList) - { - memberCache[member.NameOrPath] = member; - } + var memberCache = BuildMemberCache(accessor); foreach (var member in accessor.MemberList) { @@ -807,5 +1007,21 @@ namespace JiShe.CollectBus.IoTDB.Provider TSDataType.STRING => Convert.ToString(value), _ => Convert.ToString(value) }; + + /// + /// 缓存实体属性信息 + /// + /// + /// + /// + private Dictionary BuildMemberCache(ISourceEntityAccessor accessor) + { + var cache = new Dictionary(StringComparer.Ordinal); + foreach (var member in accessor.MemberList) + { + cache[member.NameOrPath] = member; + } + return cache; + } } } diff --git a/services/JiShe.CollectBus.Application/ScheduledMeterReading/BasicScheduledMeterReadingService.cs b/services/JiShe.CollectBus.Application/ScheduledMeterReading/BasicScheduledMeterReadingService.cs index 32a1d7c..bab879d 100644 --- a/services/JiShe.CollectBus.Application/ScheduledMeterReading/BasicScheduledMeterReadingService.cs +++ b/services/JiShe.CollectBus.Application/ScheduledMeterReading/BasicScheduledMeterReadingService.cs @@ -7,8 +7,9 @@ using JiShe.CollectBus.Common.Enums; using JiShe.CollectBus.Common.Extensions; using JiShe.CollectBus.Common.Helpers; using JiShe.CollectBus.Common.Models; +using JiShe.CollectBus.DataChannels; +using JiShe.CollectBus.DataMigration.Options; using JiShe.CollectBus.GatherItem; -using JiShe.CollectBus.IoTDB.Context; using JiShe.CollectBus.IoTDB.Interface; using JiShe.CollectBus.IoTDB.Model; using JiShe.CollectBus.IoTDB.Options; @@ -17,8 +18,8 @@ using JiShe.CollectBus.IotSystems.Ammeters; using JiShe.CollectBus.IotSystems.MeterReadingRecords; using JiShe.CollectBus.IotSystems.Watermeter; using JiShe.CollectBus.Kafka.Internal; -using JiShe.CollectBus.Kafka.Producer; using JiShe.CollectBus.Protocol.Interfaces; +using JiShe.CollectBus.Protocol.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; @@ -26,14 +27,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using JiShe.CollectBus.Protocol.Models; -using System.Threading.Channels; -using static IdentityModel.ClaimComparer; -using JiShe.CollectBus.DataChannels; -using JiShe.CollectBus.DataMigration.Options; -using static System.Runtime.InteropServices.JavaScript.JSType; -using static System.Formats.Asn1.AsnWriter; -using System.Threading; namespace JiShe.CollectBus.ScheduledMeterReading { @@ -329,7 +322,7 @@ namespace JiShe.CollectBus.ScheduledMeterReading /// public virtual async Task InitAmmeterCacheData(string gatherCode = "") { - return; + //return; // 创建取消令牌源 //var cts = new CancellationTokenSource(); @@ -744,7 +737,7 @@ namespace JiShe.CollectBus.ScheduledMeterReading ItemCode = tempItem, DataTimeMark = new Protocol.DataTimeMark() { - Density = ammeterInfo.TimeDensity,//todo 转换成协议的值 + Density = ammeterInfo.TimeDensity.GetDensity(),//转换成协议的值 Point = 1, DataTime = timestamps, } @@ -1354,7 +1347,6 @@ namespace JiShe.CollectBus.ScheduledMeterReading } List taskList = new List(); - var metadata = await _dbProvider.GetMetadata(); var itemCode = T37612012PacketItemCodeConst.AFN09HFN01H; //var subItemCode = T6452007PacketItemCodeConst.C08; @@ -1673,7 +1665,7 @@ namespace JiShe.CollectBus.ScheduledMeterReading IsReceived = false, ScoreValue = $"{ammeterInfo.FocusAddress}.{taskMark}".Md5Fun(), }; - } + } #endregion } diff --git a/shared/JiShe.CollectBus.Common/Helpers/CommonHelper.cs b/shared/JiShe.CollectBus.Common/Helpers/CommonHelper.cs index 648c1aa..e3177a1 100644 --- a/shared/JiShe.CollectBus.Common/Helpers/CommonHelper.cs +++ b/shared/JiShe.CollectBus.Common/Helpers/CommonHelper.cs @@ -859,6 +859,21 @@ namespace JiShe.CollectBus.Common.Helpers } - + /// + /// 采集频率转换为集中器采集密度 + /// + /// + /// + public static int GetDensity(this int timeDensity) => + timeDensity switch + { + 0 => 0,//无 + 1 => 255,//1分钟 + 5 => 245,//5分钟 + 15 => 1,//15分钟 + 30 => 2,//30分钟 + 60 => 3,//60分钟 + _ => -1//采集项本身无密度位 + }; } } diff --git a/web/JiShe.CollectBus.Host/Pages/Monitor.cshtml b/web/JiShe.CollectBus.Host/Pages/Monitor.cshtml index 30e91e8..9509b08 100644 --- a/web/JiShe.CollectBus.Host/Pages/Monitor.cshtml +++ b/web/JiShe.CollectBus.Host/Pages/Monitor.cshtml @@ -15,8 +15,7 @@ - 后端服务 - + 后端服务 diff --git a/web/JiShe.CollectBus.Host/appsettings.json b/web/JiShe.CollectBus.Host/appsettings.json index 7034d26..43f1723 100644 --- a/web/JiShe.CollectBus.Host/appsettings.json +++ b/web/JiShe.CollectBus.Host/appsettings.json @@ -141,7 +141,7 @@ } }, "ServerApplicationOptions": { - "ServerTagName": "JiSheCollectBus99", + "ServerTagName": "JiSheCollectBus4", "SystemType": "Energy", "FirstCollectionTime": "2025-04-28 15:07:00", "AutomaticVerificationTime": "16:07:00",