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 GroWattInverter.MessageEntity; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Timers; using System.Text; namespace GroWattInverter { /// /// 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:古瑞瓦特 光伏逆变器lang-en-us:GroWatt PhotoVoltaic Inverter", "lang-zh-cn:用于驱动 古瑞瓦特-X及储能系列逆变器,日志名称 DynamicPrivate_GroWattInverterlang-en-us:Used for driven 古瑞瓦特-X及储能系列逆变器 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: // 45 Sys Year 系统时间-年 // 46 Sys Month 系统时间 // 47 Sys Day 系 // 48 Sys Hour 系统时间-小时 // 49 Sys Min 系统时间-分 // 50 Sys Sec 系统时间-秒 return new OutgoingQueryMessage() { SlaveAddress = target.SlaveAddress, FunctionCode = FunctionCodeEnum.保持寄存器, StartingRegAddress = 45, NoOfRegAddress = 6 }; } 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); } else if (targetDevice.IsOnline == false) { targetDevice.LastIncomingMessageReceivedTime = DateTime.Now; targetDevice.IsOnline = true; var deviceDetailInfoResponse = await context.Outgoing.WriteAsync( new OutgoingQueryMessage() { SlaveAddress = targetDevice.SlaveAddress, FunctionCode = FunctionCodeEnum.保持寄存器, StartingRegAddress = 34, NoOfRegAddress = 1 }, (testRequest, testResponse) => testResponse is IncomingQueryMessage, 5000) as IncomingQueryMessage; if (deviceDetailInfoResponse != null) { var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor, new GenericAlarm() { Title = $"地址为{targetDevice.SlaveAddress}的光伏逆变器成功连接", Category = $"光伏逆变器", Detail = $"地址为{targetDevice.SlaveAddress}的光伏逆变器于 {DateTime.Now} 成功连接,制造商信息: {Encoding.ASCII.GetString(deviceDetailInfoResponse.RawData.ToArray()) }", 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, FunctionCode = FunctionCodeEnum.只读寄存器, StartingRegAddress = 53, NoOfRegAddress = 2 }, (_, testResponse) => testResponse is IncomingQueryMessage, 6000) as IncomingQueryMessage; if (有功response == null) throw new TimeoutException($"ReadRealTimeDataAsync read 有功response timedout for device with slaveAddress: {deviceSlaveAddress}"); var 无功response = await this.context.Outgoing.WriteAsync(new OutgoingQueryMessage() { SlaveAddress = deviceSlaveAddress, FunctionCode = FunctionCodeEnum.只读寄存器, StartingRegAddress = 232, NoOfRegAddress = 2 }, (_, testResponse) => testResponse is IncomingQueryMessage, 6000) as IncomingQueryMessage; if (无功response == null) throw new TimeoutException($"ReadRealTimeDataAsync read 无功response timedout for device with slaveAddress: {deviceSlaveAddress}"); var realtimeData = this.Parse(有功response, 无功response); 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 Parse(IncomingQueryMessage 有功response, IncomingQueryMessage 无功response) { //0.1kWh var 日发电量 = ((decimal)(BitConverter.ToUInt32(有功response.RawData.Take(4).Reverse().ToArray()))) / 10; //0.1kWh decimal 日无功发电量 = -9999; if (无功response != null) 日无功发电量 = ((decimal)(BitConverter.ToUInt32(无功response.RawData.Take(4).Reverse().ToArray()))) / 10; return new InverterDeviceRealTimeData() { //Device = device, 日有功发电量 = 日发电量, 日无功发电量 = 日无功发电量, //Description = $"设备类型编码: 0x{设备类型编码.ToHexLogString()}, 额定有功功率: {额定有功功率}(kW), 输出类型: {输出类型}, 总发电量: {总发电量}(kWh)" }; } } }