DeviceGroupHandler.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. using Edge.Core.IndustryStandardInterface.PhotoVoltaicInverter;
  2. using Edge.Core.Parser.BinaryParser.Util;
  3. using Edge.Core.Processor;
  4. using Edge.Core.Processor.Communicator;
  5. using Edge.Core.Processor.Dispatcher.Attributes;
  6. using Edge.Core.UniversalApi;
  7. using Microsoft.Extensions.DependencyInjection;
  8. using Microsoft.Extensions.Logging;
  9. using Microsoft.Extensions.Logging.Abstractions;
  10. using SunGrowInverter.MessageEntity;
  11. using System;
  12. using System.Collections;
  13. using System.Collections.Generic;
  14. using System.Linq;
  15. using System.Threading.Tasks;
  16. using System.Timers;
  17. namespace SunGrowInverter
  18. {
  19. /// <summary>
  20. /// the device could support RS485 and Ethernet.
  21. /// For RS485, multiple devices could be on the bus, that's why here implement the handler as a group.
  22. /// For Ethernet, each handler can only handle one device.
  23. /// </summary>
  24. [MetaPartsRequired(typeof(HalfDuplexActivePollingDeviceProcessor<,>))]
  25. [MetaPartsRequired(typeof(ComPortCommunicator<>))]
  26. [MetaPartsRequired(typeof(TcpClientCommunicator<>))]
  27. [MetaPartsDescriptor(
  28. "lang-zh-cn:SunGrow 光伏逆变器lang-en-us:SunGrow PhotoVoltaic Inverter",
  29. "lang-zh-cn:用于驱动 SunGrow 光伏逆变器,日志名称 DynamicPrivate_SunGrowInverterlang-en-us:Used for driven SunGrow PhotoVoltaic Inverter",
  30. new[] { "lang-zh-cn:光伏lang-en-us:PhotoVoltaic" })]
  31. public class DeviceGroupHandler : TestableActivePollingDeviceHandler<byte[], SunGrowInverter.MessageEntity.MessageBase>, IPhotoVoltaicInverterHandler, IDisposable
  32. {
  33. private List<InverterDevice> inverterDevices = new List<InverterDevice>();
  34. private IContext<byte[], MessageBase> context;
  35. private ILogger logger = NullLogger.Instance;
  36. private DeviceGroupConfigV1 deviceGroupConfig;
  37. private IServiceProvider services;
  38. private int deviceTreatAsOfflineTimeThresholdByMs = 8000;
  39. public event EventHandler OnDeviceError;
  40. public class DeviceGroupConfigV1
  41. {
  42. public List<DeviceConfigV1> DeviceConfigs { get; set; }
  43. }
  44. public class DeviceConfigV1
  45. {
  46. public string DeviceName { get; set; }
  47. public byte SlaveAddress { get; set; }
  48. public string Description { get; set; }
  49. }
  50. [ParamsJsonSchemas("ctorParamsJsonSchema")]
  51. public DeviceGroupHandler(DeviceGroupConfigV1 deviceGroupConfig, IServiceProvider services)
  52. {
  53. this.deviceGroupConfig = deviceGroupConfig;
  54. this.services = services;
  55. var loggerFactory = services.GetRequiredService<ILoggerFactory>();
  56. this.logger = loggerFactory.CreateLogger("DynamicPrivate_SunGrowInverter");
  57. this.inverterDevices = this.deviceGroupConfig.DeviceConfigs.Select(dc => new InverterDevice(dc.DeviceName, dc.SlaveAddress)).ToList();
  58. }
  59. public override void Init(IContext<byte[], MessageBase> context)
  60. {
  61. base.Init(context);
  62. this.context = context;
  63. var timeWindowWithActivePollingOutgoing =
  64. this.context.Outgoing as TimeWindowWithActivePollingOutgoing<byte[], MessageBase>;
  65. int previousPolledDeviceIndex = 0;
  66. timeWindowWithActivePollingOutgoing.PollingMsgProducer = () =>
  67. {
  68. try
  69. {
  70. //no need too fast to detect online&offline state
  71. if (DateTime.Now.Second % 3 == 0)
  72. {
  73. foreach (var d in this.inverterDevices)
  74. {
  75. var silentLastingTime = DateTime.Now.Subtract(d.LastIncomingMessageReceivedTime ?? DateTime.MinValue).TotalMilliseconds;
  76. if (d.IsOnline
  77. && silentLastingTime >= this.deviceTreatAsOfflineTimeThresholdByMs)
  78. {
  79. d.IsOnline = false;
  80. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  81. var __ = universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor,
  82. new GenericAlarm()
  83. {
  84. Title = $"地址为{d.SlaveAddress}的光伏逆变器连接断开",
  85. Category = $"光伏逆变器",
  86. Detail = $"地址为{d.SlaveAddress}的光伏逆变器于 {DateTime.Now} 连接断开, 上次收到它的消息是 {(int)(silentLastingTime / 1000)}秒 前",
  87. Severity = GenericAlarmSeverity.Warning
  88. }, ga => ga.Detail,
  89. ga => ga.Detail).Result;
  90. }
  91. }
  92. }
  93. if (this.deviceGroupConfig.DeviceConfigs.Count <= previousPolledDeviceIndex)
  94. previousPolledDeviceIndex = 0;
  95. var target = this.deviceGroupConfig.DeviceConfigs[previousPolledDeviceIndex++];
  96. //poll 设备类型编码
  97. return new OutgoingQueryMessage() { SlaveAddress = target.SlaveAddress, FunctionCode = FunctionCodeEnum.只读寄存器, StartingRegAddress = 4999, NoOfRegAddress = 1 };
  98. }
  99. catch (Exception exxx)
  100. {
  101. logger.LogError($"Exceptioned (previousPolledHandlerIndex: {previousPolledDeviceIndex}): {exxx}");
  102. return null;
  103. }
  104. };
  105. }
  106. public override async Task Process(IContext<byte[], MessageBase> context)
  107. {
  108. this.context = context;
  109. var targetDevice = this.inverterDevices.FirstOrDefault(id => id.SlaveAddress == context.Incoming.Message.SlaveAddress);
  110. if (targetDevice == null)
  111. {
  112. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  113. await universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor,
  114. new GenericAlarm()
  115. {
  116. Title = $"地址为{targetDevice.SlaveAddress}的光伏逆变器未配置",
  117. Category = $"光伏逆变器",
  118. Detail = $"地址为{targetDevice.SlaveAddress}的光伏逆变器:{targetDevice.Description ?? ""} 于 {DateTime.Now} 成功连接,但本地未对其进行配置,请打开FCC配置页面添加此设备",
  119. Severity = GenericAlarmSeverity.Warning
  120. }, ga => ga.Detail,
  121. ga => ga.Detail);
  122. await base.Process(context);
  123. return;
  124. }
  125. targetDevice.LastIncomingMessageReceivedTime = DateTime.Now;
  126. if (targetDevice.IsOnline == false)
  127. {
  128. targetDevice.LastIncomingMessageReceivedTime = DateTime.Now;
  129. targetDevice.IsOnline = true;
  130. //保持寄存器, 5000-5005 are 系统时钟, sample raw response body: 0x07 0xE6 0x00 0x08 0x00 0x1D 0x00 0x0D 0x00 0x1D 0x00 0x1A
  131. var deviceDetailInfoResponse = await context.Outgoing.WriteAsync(
  132. new OutgoingQueryMessage() { SlaveAddress = targetDevice.SlaveAddress, FunctionCode = FunctionCodeEnum.保持寄存器, StartingRegAddress = 4999, NoOfRegAddress = 6 },
  133. (testRequest, testResponse) => testResponse is IncomingQueryMessage, 5000) as IncomingQueryMessage;
  134. if (deviceDetailInfoResponse != null)
  135. {
  136. var device_year = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Take(2).Reverse().ToArray());
  137. var device_month = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Skip(2).Take(2).Reverse().ToArray());
  138. var device_day_in_month = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Skip(4).Take(2).Reverse().ToArray());
  139. var device_hour = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Skip(6).Take(2).Reverse().ToArray());
  140. var device_min = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Skip(8).Take(2).Reverse().ToArray());
  141. var device_sec = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Skip(10).Take(2).Reverse().ToArray());
  142. //var info = this.ParseFromIncomingQueryMessage_For_Request_With_StartingRegAddress4999_NoOfRegAddress10(targetDevice, deviceDetailInfoResponse);
  143. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  144. await universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor,
  145. new GenericAlarm()
  146. {
  147. Title = $"地址为{targetDevice.SlaveAddress}的光伏逆变器成功连接",
  148. Category = $"光伏逆变器",
  149. Detail = $"地址为{targetDevice.SlaveAddress}的光伏逆变器于 {DateTime.Now} 成功连接,设备侧的时间设置为: {device_year}年{device_month}月{device_day_in_month}日 - {device_hour}时{device_min}分{device_sec}秒",
  150. Severity = GenericAlarmSeverity.Information
  151. }, ga => ga.Detail,
  152. ga => ga.Detail);
  153. }
  154. else
  155. {
  156. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  157. await universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor,
  158. new GenericAlarm()
  159. {
  160. Title = $"地址为{targetDevice.SlaveAddress}的光伏逆变器成功连接",
  161. Category = $"光伏逆变器",
  162. Detail = $"地址为{targetDevice.SlaveAddress}的光伏逆变器于 {DateTime.Now} 成功连接, 但获取设置详细信息失败",
  163. Severity = GenericAlarmSeverity.Information
  164. }, ga => ga.Detail,
  165. ga => ga.Detail);
  166. }
  167. }
  168. await base.Process(context);
  169. return;
  170. }
  171. public void Dispose()
  172. {
  173. }
  174. public IEnumerable<InverterDevice> GetDevices()
  175. {
  176. return this.inverterDevices;
  177. }
  178. [UniversalApi]
  179. public virtual async Task<InverterDeviceRealTimeData> ReadRealTimeDataAsync(byte deviceSlaveAddress)
  180. {
  181. var response = await this.context.Outgoing.WriteAsync(new OutgoingQueryMessage()
  182. {
  183. SlaveAddress = deviceSlaveAddress,
  184. StartingRegAddress = 4999,
  185. NoOfRegAddress = 11
  186. }, (_, testResponse) => testResponse is IncomingQueryMessage, 6000) as IncomingQueryMessage;
  187. if (response == null)
  188. throw new TimeoutException($"ReadRunningInfoAsync timedout for device with slaveAddress: {deviceSlaveAddress}");
  189. var realtimeData = this.ParseFromIncomingQueryMessage_For_Request_With_StartingRegAddress4999_NoOfRegAddress10(response);
  190. //总有功功率 5031~5032 U32 W
  191. //总无功功率 5033~5034 S32 var
  192. //总功率因数 5035 S16 0.001 正功率因数代表超前,负功率因数代表滞后
  193. //电网频率 5036 U16 0.1Hz
  194. //RAW response sample: 0x01 03 0C FF FF FF FF FF FF 00 03 00 55 00 55 53 51
  195. var extraReadingResponse = await this.context.Outgoing.WriteAsync(new OutgoingQueryMessage()
  196. {
  197. SlaveAddress = deviceSlaveAddress,
  198. StartingRegAddress = 5030,
  199. NoOfRegAddress = 6
  200. }, (_, testResponse) => testResponse is IncomingQueryMessage, 6000) as IncomingQueryMessage;
  201. if (extraReadingResponse != null)
  202. {
  203. var 总有功功率 = BitConverter.ToUInt32(extraReadingResponse.RawData.Take(4).Reverse().ToArray());
  204. var 总无功功率 = BitConverter.ToInt32(extraReadingResponse.RawData.Skip(4).Take(4).Reverse().ToArray());
  205. var 总功率因数 = ((decimal)BitConverter.ToInt16(extraReadingResponse.RawData.Skip(8).Take(2).Reverse().ToArray())) / 1000;
  206. var 电网频率 = ((decimal)BitConverter.ToUInt16(extraReadingResponse.RawData.Skip(10).Take(2).Reverse().ToArray())) / 10;
  207. realtimeData.Description += $", 总有功功率: {总有功功率}(W), 总无功功率: {总无功功率}(var), 总功率因数: {总功率因数}, 电网频率:{电网频率}(Hz)";
  208. }
  209. return realtimeData;
  210. }
  211. [UniversalApi]
  212. public async Task<object> RawSendOutgoingQueryMessageAsync(byte deviceSlaveAddress, FunctionCodeEnum functionCode, int startingRegAddress, byte noOfRegAddress)
  213. {
  214. var response = await this.context.Outgoing.WriteAsync(new OutgoingQueryMessage()
  215. {
  216. SlaveAddress = deviceSlaveAddress,
  217. FunctionCode = functionCode,
  218. StartingRegAddress = startingRegAddress,
  219. NoOfRegAddress = noOfRegAddress
  220. }, (_, testResponse) => testResponse is IncomingQueryMessage, 6000) as IncomingQueryMessage;
  221. if (response == null)
  222. throw new TimeoutException("long time no see incoming response");
  223. return response.ToLogString();
  224. }
  225. private InverterDeviceRealTimeData ParseFromIncomingQueryMessage_For_Request_With_StartingRegAddress4999_NoOfRegAddress10(IncomingQueryMessage incomingQueryMessage)
  226. {
  227. //sample response from doc:
  228. //01 04 14 01 32 00 28 00 00 00 00 00 05 00 00 00 26 00 00 00 00 00 00 56 EA
  229. //注:读取 SG60KU-M 设备类型码为 0x0132,额定输出功率 4.0 kW,两相,日发电量为 0,总发电量为 5 kWh,总运行时间为 38h,机内空气温度为 0,机内变压器温度为 0。
  230. //sample from real device:
  231. // 设备类型编码 额定有功功率(0.1kW) 输出类型 日发电量(0.1kW) 总发电量(kWh) 总并网运行时间(h) 机内空气温度(0.1c) 总视在功率(VA)
  232. //0x0x01 04 16 2C 02 01 F4 00 01 08 3C 2E 7C 00 00 02 DF 00 00 02 27 6A 9F 00 00 CB 69
  233. //{
  234. // "日有功发电量": 133,
  235. // "日无功发电量": 0,
  236. // "总发电量": 11480,
  237. // "总运行时间": 699,
  238. // "机内空气温度": 0,
  239. // "机内变压器温度": 0,
  240. // "Description": "设备类型编码: 0x2C 02, 额定有功功率: 50(kW), 输出类型: 三相三线, 总发电量: 11480(kWh)"
  241. //}
  242. var 设备类型编码 = incomingQueryMessage.RawData.Take(2);
  243. //0.1kW
  244. var 额定有功功率 = ((decimal)BitConverter.ToUInt16(incomingQueryMessage.RawData.Skip(2).Take(2).Reverse().ToArray())) / 10;
  245. var raw_输出类型 = BitConverter.ToUInt16(incomingQueryMessage.RawData.Skip(4).Take(2).ToArray());
  246. var 输出类型 = raw_输出类型 == 0 ? "单相" : (raw_输出类型 == 1 ? "三相四线" : "三相三线");
  247. //0.1kWh
  248. var 日发电量 = ((decimal)BitConverter.ToUInt16(incomingQueryMessage.RawData.Skip(6).Take(2).Reverse().ToArray())) / 10;
  249. //kWh
  250. var 总发电量 = ((decimal)BitConverter.ToUInt32(incomingQueryMessage.RawData.Skip(8).Take(4).Reverse().ToArray()));
  251. //h
  252. var 总并网运行时间 = ((decimal)BitConverter.ToUInt32(incomingQueryMessage.RawData.Skip(12).Take(4).Reverse().ToArray()));
  253. //0.1℃
  254. var 机内空气温度 = ((decimal)BitConverter.ToInt16(incomingQueryMessage.RawData.Skip(16).Take(2).Reverse().ToArray())) / 10;
  255. //VA
  256. var 总视在功率 = ((decimal)BitConverter.ToUInt32(incomingQueryMessage.RawData.Skip(18).Take(4).Reverse().ToArray()));
  257. return new InverterDeviceRealTimeData()
  258. {
  259. //Device = device,
  260. 日有功发电量 = 日发电量,
  261. 总运行时间 = 总并网运行时间,
  262. 机内空气温度 = 机内空气温度,
  263. 总发电量 = 总发电量,
  264. Description = $"设备类型编码: 0x{设备类型编码.ToHexLogString()}, 额定有功功率: {额定有功功率}(kW), 输出类型: {输出类型}, 总发电量: {总发电量}(kWh), 总并网运行时间: {总并网运行时间}(h), 总视在功率: {总视在功率}(VA)"
  265. };
  266. }
  267. }
  268. }