| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- using Edge.Core.IndustryStandardInterface.PhotoVoltaicInverter;
- using Edge.Core.Parser.BinaryParser.Util;
- using Edge.Core.Processor;
- using Edge.Core.Processor.Communicator;
- using Edge.Core.Processor.Dispatcher.Attributes;
- using Edge.Core.UniversalApi;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Logging;
- using Microsoft.Extensions.Logging.Abstractions;
- using SunGrowInverter.MessageEntity;
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading.Tasks;
- using System.Timers;
- namespace SunGrowInverter
- {
- /// <summary>
- /// the device could support RS485 and Ethernet.
- /// For RS485, multiple devices could be on the bus, that's why here implement the handler as a group.
- /// For Ethernet, each handler can only handle one device.
- /// </summary>
- [MetaPartsRequired(typeof(HalfDuplexActivePollingDeviceProcessor<,>))]
- [MetaPartsRequired(typeof(ComPortCommunicator<>))]
- [MetaPartsRequired(typeof(TcpClientCommunicator<>))]
- [MetaPartsDescriptor(
- "lang-zh-cn:SunGrow 光伏逆变器lang-en-us:SunGrow PhotoVoltaic Inverter",
- "lang-zh-cn:用于驱动 SunGrow 光伏逆变器,日志名称 DynamicPrivate_SunGrowInverterlang-en-us:Used for driven SunGrow PhotoVoltaic Inverter",
- new[] { "lang-zh-cn:光伏lang-en-us:PhotoVoltaic" })]
- public class DeviceGroupHandler : TestableActivePollingDeviceHandler<byte[], SunGrowInverter.MessageEntity.MessageBase>, IPhotoVoltaicInverterHandler, IDisposable
- {
- private List<InverterDevice> inverterDevices = new List<InverterDevice>();
- private IContext<byte[], MessageBase> context;
- private ILogger logger = NullLogger.Instance;
- private DeviceGroupConfigV1 deviceGroupConfig;
- private IServiceProvider services;
- private int deviceTreatAsOfflineTimeThresholdByMs = 8000;
- public event EventHandler OnDeviceError;
- public class DeviceGroupConfigV1
- {
- public List<DeviceConfigV1> DeviceConfigs { get; set; }
- }
- public class DeviceConfigV1
- {
- public string DeviceName { get; set; }
- public byte SlaveAddress { get; set; }
- public string Description { get; set; }
- }
- [ParamsJsonSchemas("ctorParamsJsonSchema")]
- public DeviceGroupHandler(DeviceGroupConfigV1 deviceGroupConfig, IServiceProvider services)
- {
- this.deviceGroupConfig = deviceGroupConfig;
- this.services = services;
- var loggerFactory = services.GetRequiredService<ILoggerFactory>();
- this.logger = loggerFactory.CreateLogger("DynamicPrivate_SunGrowInverter");
- this.inverterDevices = this.deviceGroupConfig.DeviceConfigs.Select(dc => new InverterDevice(dc.DeviceName, dc.SlaveAddress)).ToList();
- }
- public override void Init(IContext<byte[], MessageBase> context)
- {
- base.Init(context);
- this.context = context;
- var timeWindowWithActivePollingOutgoing =
- this.context.Outgoing as TimeWindowWithActivePollingOutgoing<byte[], MessageBase>;
- int previousPolledDeviceIndex = 0;
- timeWindowWithActivePollingOutgoing.PollingMsgProducer = () =>
- {
- try
- {
- //no need too fast to detect online&offline state
- if (DateTime.Now.Second % 3 == 0)
- {
- foreach (var d in this.inverterDevices)
- {
- var silentLastingTime = DateTime.Now.Subtract(d.LastIncomingMessageReceivedTime ?? DateTime.MinValue).TotalMilliseconds;
- if (d.IsOnline
- && silentLastingTime >= this.deviceTreatAsOfflineTimeThresholdByMs)
- {
- d.IsOnline = false;
- var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
- var __ = universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor,
- new GenericAlarm()
- {
- Title = $"地址为{d.SlaveAddress}的光伏逆变器连接断开",
- Category = $"光伏逆变器",
- Detail = $"地址为{d.SlaveAddress}的光伏逆变器于 {DateTime.Now} 连接断开, 上次收到它的消息是 {(int)(silentLastingTime / 1000)}秒 前",
- Severity = GenericAlarmSeverity.Warning
- }, ga => ga.Detail,
- ga => ga.Detail).Result;
- }
- }
- }
- if (this.deviceGroupConfig.DeviceConfigs.Count <= previousPolledDeviceIndex)
- previousPolledDeviceIndex = 0;
- var target = this.deviceGroupConfig.DeviceConfigs[previousPolledDeviceIndex++];
- //poll 设备类型编码
- return new OutgoingQueryMessage() { SlaveAddress = target.SlaveAddress, FunctionCode = FunctionCodeEnum.只读寄存器, StartingRegAddress = 4999, NoOfRegAddress = 1 };
- }
- catch (Exception exxx)
- {
- logger.LogError($"Exceptioned (previousPolledHandlerIndex: {previousPolledDeviceIndex}): {exxx}");
- return null;
- }
- };
- }
- public override async Task Process(IContext<byte[], MessageBase> context)
- {
- this.context = context;
- var targetDevice = this.inverterDevices.FirstOrDefault(id => id.SlaveAddress == context.Incoming.Message.SlaveAddress);
- if (targetDevice == null)
- {
- var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
- await universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor,
- new GenericAlarm()
- {
- Title = $"地址为{targetDevice.SlaveAddress}的光伏逆变器未配置",
- Category = $"光伏逆变器",
- Detail = $"地址为{targetDevice.SlaveAddress}的光伏逆变器:{targetDevice.Description ?? ""} 于 {DateTime.Now} 成功连接,但本地未对其进行配置,请打开FCC配置页面添加此设备",
- Severity = GenericAlarmSeverity.Warning
- }, ga => ga.Detail,
- ga => ga.Detail);
- await base.Process(context);
- return;
- }
- targetDevice.LastIncomingMessageReceivedTime = DateTime.Now;
- if (targetDevice.IsOnline == false)
- {
- targetDevice.LastIncomingMessageReceivedTime = DateTime.Now;
- targetDevice.IsOnline = true;
- //保持寄存器, 5000-5005 are 系统时钟, sample raw response body: 0x07 0xE6 0x00 0x08 0x00 0x1D 0x00 0x0D 0x00 0x1D 0x00 0x1A
- var deviceDetailInfoResponse = await context.Outgoing.WriteAsync(
- new OutgoingQueryMessage() { SlaveAddress = targetDevice.SlaveAddress, FunctionCode = FunctionCodeEnum.保持寄存器, StartingRegAddress = 4999, NoOfRegAddress = 6 },
- (testRequest, testResponse) => testResponse is IncomingQueryMessage, 5000) as IncomingQueryMessage;
- if (deviceDetailInfoResponse != null)
- {
- var device_year = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Take(2).Reverse().ToArray());
- var device_month = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Skip(2).Take(2).Reverse().ToArray());
- var device_day_in_month = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Skip(4).Take(2).Reverse().ToArray());
- var device_hour = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Skip(6).Take(2).Reverse().ToArray());
- var device_min = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Skip(8).Take(2).Reverse().ToArray());
- var device_sec = BitConverter.ToUInt16(deviceDetailInfoResponse.RawData.Skip(10).Take(2).Reverse().ToArray());
- //var info = this.ParseFromIncomingQueryMessage_For_Request_With_StartingRegAddress4999_NoOfRegAddress10(targetDevice, deviceDetailInfoResponse);
- var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
- await universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor,
- new GenericAlarm()
- {
- Title = $"地址为{targetDevice.SlaveAddress}的光伏逆变器成功连接",
- Category = $"光伏逆变器",
- Detail = $"地址为{targetDevice.SlaveAddress}的光伏逆变器于 {DateTime.Now} 成功连接,设备侧的时间设置为: {device_year}年{device_month}月{device_day_in_month}日 - {device_hour}时{device_min}分{device_sec}秒",
- Severity = GenericAlarmSeverity.Information
- }, ga => ga.Detail,
- ga => ga.Detail);
- }
- else
- {
- var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
- await universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor,
- new GenericAlarm()
- {
- Title = $"地址为{targetDevice.SlaveAddress}的光伏逆变器成功连接",
- Category = $"光伏逆变器",
- Detail = $"地址为{targetDevice.SlaveAddress}的光伏逆变器于 {DateTime.Now} 成功连接, 但获取设置详细信息失败",
- Severity = GenericAlarmSeverity.Information
- }, ga => ga.Detail,
- ga => ga.Detail);
- }
- }
- await base.Process(context);
- return;
- }
- public void Dispose()
- {
- }
- public IEnumerable<InverterDevice> GetDevices()
- {
- return this.inverterDevices;
- }
- [UniversalApi]
- public virtual async Task<InverterDeviceRealTimeData> ReadRealTimeDataAsync(byte deviceSlaveAddress)
- {
- var response = await this.context.Outgoing.WriteAsync(new OutgoingQueryMessage()
- {
- SlaveAddress = deviceSlaveAddress,
- StartingRegAddress = 4999,
- NoOfRegAddress = 11
- }, (_, testResponse) => testResponse is IncomingQueryMessage, 6000) as IncomingQueryMessage;
- if (response == null)
- throw new TimeoutException($"ReadRunningInfoAsync timedout for device with slaveAddress: {deviceSlaveAddress}");
- var realtimeData = this.ParseFromIncomingQueryMessage_For_Request_With_StartingRegAddress4999_NoOfRegAddress10(response);
- //总有功功率 5031~5032 U32 W
- //总无功功率 5033~5034 S32 var
- //总功率因数 5035 S16 0.001 正功率因数代表超前,负功率因数代表滞后
- //电网频率 5036 U16 0.1Hz
- //RAW response sample: 0x01 03 0C FF FF FF FF FF FF 00 03 00 55 00 55 53 51
- var extraReadingResponse = await this.context.Outgoing.WriteAsync(new OutgoingQueryMessage()
- {
- SlaveAddress = deviceSlaveAddress,
- StartingRegAddress = 5030,
- NoOfRegAddress = 6
- }, (_, testResponse) => testResponse is IncomingQueryMessage, 6000) as IncomingQueryMessage;
- if (extraReadingResponse != null)
- {
- var 总有功功率 = BitConverter.ToUInt32(extraReadingResponse.RawData.Take(4).Reverse().ToArray());
- var 总无功功率 = BitConverter.ToInt32(extraReadingResponse.RawData.Skip(4).Take(4).Reverse().ToArray());
- var 总功率因数 = ((decimal)BitConverter.ToInt16(extraReadingResponse.RawData.Skip(8).Take(2).Reverse().ToArray())) / 1000;
- var 电网频率 = ((decimal)BitConverter.ToUInt16(extraReadingResponse.RawData.Skip(10).Take(2).Reverse().ToArray())) / 10;
- realtimeData.Description += $", 总有功功率: {总有功功率}(W), 总无功功率: {总无功功率}(var), 总功率因数: {总功率因数}, 电网频率:{电网频率}(Hz)";
- }
- return realtimeData;
- }
- [UniversalApi]
- public async Task<object> RawSendOutgoingQueryMessageAsync(byte deviceSlaveAddress, FunctionCodeEnum functionCode, int startingRegAddress, byte noOfRegAddress)
- {
- var response = await this.context.Outgoing.WriteAsync(new OutgoingQueryMessage()
- {
- SlaveAddress = deviceSlaveAddress,
- FunctionCode = functionCode,
- StartingRegAddress = startingRegAddress,
- NoOfRegAddress = noOfRegAddress
- }, (_, testResponse) => testResponse is IncomingQueryMessage, 6000) as IncomingQueryMessage;
- if (response == null)
- throw new TimeoutException("long time no see incoming response");
- return response.ToLogString();
- }
- private InverterDeviceRealTimeData ParseFromIncomingQueryMessage_For_Request_With_StartingRegAddress4999_NoOfRegAddress10(IncomingQueryMessage incomingQueryMessage)
- {
- //sample response from doc:
- //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
- //注:读取 SG60KU-M 设备类型码为 0x0132,额定输出功率 4.0 kW,两相,日发电量为 0,总发电量为 5 kWh,总运行时间为 38h,机内空气温度为 0,机内变压器温度为 0。
- //sample from real device:
- // 设备类型编码 额定有功功率(0.1kW) 输出类型 日发电量(0.1kW) 总发电量(kWh) 总并网运行时间(h) 机内空气温度(0.1c) 总视在功率(VA)
- //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
- //{
- // "日有功发电量": 133,
- // "日无功发电量": 0,
- // "总发电量": 11480,
- // "总运行时间": 699,
- // "机内空气温度": 0,
- // "机内变压器温度": 0,
- // "Description": "设备类型编码: 0x2C 02, 额定有功功率: 50(kW), 输出类型: 三相三线, 总发电量: 11480(kWh)"
- //}
- var 设备类型编码 = incomingQueryMessage.RawData.Take(2);
- //0.1kW
- var 额定有功功率 = ((decimal)BitConverter.ToUInt16(incomingQueryMessage.RawData.Skip(2).Take(2).Reverse().ToArray())) / 10;
- var raw_输出类型 = BitConverter.ToUInt16(incomingQueryMessage.RawData.Skip(4).Take(2).ToArray());
- var 输出类型 = raw_输出类型 == 0 ? "单相" : (raw_输出类型 == 1 ? "三相四线" : "三相三线");
- //0.1kWh
- var 日发电量 = ((decimal)BitConverter.ToUInt16(incomingQueryMessage.RawData.Skip(6).Take(2).Reverse().ToArray())) / 10;
- //kWh
- var 总发电量 = ((decimal)BitConverter.ToUInt32(incomingQueryMessage.RawData.Skip(8).Take(4).Reverse().ToArray()));
- //h
- var 总并网运行时间 = ((decimal)BitConverter.ToUInt32(incomingQueryMessage.RawData.Skip(12).Take(4).Reverse().ToArray()));
- //0.1℃
- var 机内空气温度 = ((decimal)BitConverter.ToInt16(incomingQueryMessage.RawData.Skip(16).Take(2).Reverse().ToArray())) / 10;
- //VA
- var 总视在功率 = ((decimal)BitConverter.ToUInt32(incomingQueryMessage.RawData.Skip(18).Take(4).Reverse().ToArray()));
- return new InverterDeviceRealTimeData()
- {
- //Device = device,
- 日有功发电量 = 日发电量,
- 总运行时间 = 总并网运行时间,
- 机内空气温度 = 机内空气温度,
- 总发电量 = 总发电量,
- Description = $"设备类型编码: 0x{设备类型编码.ToHexLogString()}, 额定有功功率: {额定有功功率}(kW), 输出类型: {输出类型}, 总发电量: {总发电量}(kWh), 总并网运行时间: {总并网运行时间}(h), 总视在功率: {总视在功率}(VA)"
- };
- }
- }
- }
|