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 { /// /// 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. /// [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, IPhotoVoltaicInverterHandler, IDisposable { private List inverterDevices = new List(); private IContext 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 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(); 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 context) { base.Init(context); this.context = context; var timeWindowWithActivePollingOutgoing = this.context.Outgoing as TimeWindowWithActivePollingOutgoing; 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(); 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 context) { this.context = context; var targetDevice = this.inverterDevices.FirstOrDefault(id => id.SlaveAddress == context.Incoming.Message.SlaveAddress); if (targetDevice == null) { var universalApiHub = this.services.GetRequiredService(); 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(); 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(); 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 GetDevices() { return this.inverterDevices; } [UniversalApi] public virtual async Task 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 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)" }; } } }