public override string ProcessEventData(TheThing thing, object evnt, DateTimeOffset defaultTimestamp) { if (!(evnt is EventData)) { return "Invalid Payload"; } if (thing == null) { return "AMQP Converter does not support thing mapping"; } var eventData = evnt as EventData; DateTimeOffset time; if (eventData.Properties.ContainsKey("time")) { time = TheCommonUtils.CDate(eventData.Properties["time"]); } else if (defaultTimestamp == DateTimeOffset.MinValue) { time = eventData.SystemProperties.EnqueuedTimeUtc; } else { time = defaultTimestamp; } foreach (var prop in eventData.Properties) { var tProp = thing.SetProperty(prop.Key, prop.Value, time); //var tProp = thing.GetProperty(prop.Key, true); //tProp.cdeCTIM = time; //tProp.Value = prop.Value; //tProp.cdeE = 0x40; // NMI visible } return null; }
public void DateTimeOffsetMinMaxValueTest() { { var date = DateTime.MinValue; var dateTimeOffsetValue = TheCommonUtils.CDate(date); var expectedValue = DateTimeOffset.MinValue; Assert.AreEqual(expectedValue, dateTimeOffsetValue); } { var date = DateTime.MaxValue; var dateTimeOffsetValue = TheCommonUtils.CDate(date); var expectedValue = DateTimeOffset.MaxValue; Assert.AreEqual(expectedValue, dateTimeOffsetValue); } // TODO Figure out a way to run these tests in interesting timezones as they depend on the local machines timezone: // UTC, Berlin (UTC+1 DST), UTC-1, Casablanca (UTC with DST), UTC-12 (International dateline West), UTC+12, UTC+14 (Kiritimati Island) // For now: run one of the build machines in UTC+1, the other in PST. { var date = new DateTime(1, 1, 1, 0, 10, 0, DateTimeKind.Local); var dateTimeOffsetValue = TheCommonUtils.CDate(date); var expectedTimeZone = TimeZoneInfo.Local.GetUtcOffset(date); DateTimeOffset expectedValue; if (expectedTimeZone > TimeSpan.Zero) { expectedValue = DateTimeOffset.MinValue; } else { expectedValue = new DateTimeOffset(date); } Assert.AreEqual(expectedValue, dateTimeOffsetValue); } { var date = new DateTime(9999, 12, 31, 23, 59, 50, DateTimeKind.Local); var dateTimeOffsetValue = TheCommonUtils.CDate(date); DateTimeOffset expectedValue; if (TimeZoneInfo.Local.GetUtcOffset(date) < TimeSpan.Zero) { expectedValue = DateTimeOffset.MaxValue; } else { expectedValue = new DateTimeOffset(date); } Assert.AreEqual(expectedValue, dateTimeOffsetValue); } }
/// <summary> /// Checks if a property already matches a desired value. Sets the property and /// notifies listeners only when necessary. /// </summary> /// <typeparam name="T">Type of the property.</typeparam> /// <param name="storage">Reference to a property with both getter and setter.</param> /// <param name="value">Desired value for the property.</param> /// <param name="cdeT">Type of this property - important for comparison with old value</param> /// <param name="cdeE">Extended Flags of the value</param> /// <param name="propertyName">Name of the property used to notify listeners. This /// value is optional and can be provided automatically when invoked from compilers that /// support CallerMemberName.</param> /// <returns>True if the value was changed, false if the existing value matched the /// desired value.</returns> protected bool SetProperty <T>(ref T storage, T value, int cdeT, int cdeE, string propertyName = null) { if ((cdeE & 8) == 0) { if ((cdeE & 1) != 0) //Required in case the property is encrypted. { if (Equals(storage, value)) { return(false); } } else { switch (cdeT) { case 1: if (Math.Abs(TheCommonUtils.CDbl(storage) - TheCommonUtils.CDbl(value)) < Double.Epsilon && (storage == null && value == null || storage != null && value != null)) // 3.217: treat 0 and null as different { return(false); } break; case 2: if (TheCommonUtils.CBool(storage) == TheCommonUtils.CBool(value) && (storage == null && value == null || storage != null && value != null)) // 3.217: treat 0 and null as different { return(false); } value = (T)((object)TheCommonUtils.CBool(value)); break; case 3: if (storage != null && value != null && TheCommonUtils.CDate(storage) == TheCommonUtils.CDate(value)) { return(false); } break; case 4: //Binary Comparison - TODO:CODEREVIEW Could be very expensive to do Byte[] comparison if ((storage == null && value == null)) { return(false); // || ((storage as byte[]).GetHashCode() == (value as byte[]).GetHashCode())) return false; } break; case 5: //Function - Never Set it! return(false); case 6: if (TheCommonUtils.CGuid(storage) == TheCommonUtils.CGuid(value)) { return(false); } break; default: if (Equals(storage, value)) { return(false); } break; } } } storage = value; // CORE REVIEW Markus: SHould we clone the value here to prevent future modification? // CODE REVIEW: There is a race condition between setting mHasChanged and delivering the change notification. Should we raise the notification outside or is this mostly obsolete anyway (XAML only)? OnPropertyChanged(propertyName); return(true); }
override protected void MonitoredItem_Notification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e) { try { var tEventTag = monitoredItem.Handle as TheOPCEvent; if (tEventTag == null) { TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Internal error: invalid monitored item handle in Event", eMsgLevel.l1_Error, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}")); return; } var tEventHost = tEventTag.GetHostThing(); if (tEventHost == null) { TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Event host thing not found", eMsgLevel.l1_Error, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}")); return; } EventFieldList notification = e.NotificationValue as EventFieldList; if (notification == null) { TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Internal error: notification is not an EventFieldList", eMsgLevel.l1_Error, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}")); return; } // check the type of event. NodeId eventTypeId = ClientUtils.FindEventType(monitoredItem, notification); if (MyOPCServer.EnableOPCDataLogging) { var logInfo = new Dictionary <string, object> { { "ReceiveTime", DateTimeOffset.Now }, { "TagId", DisplayName }, { "EventTypeId", eventTypeId }, { "Value", notification.EventFields.Aggregate("", (s, ef) => $"{s} [{ef.TypeInfo},{ef.Value}]") }, { "Server", notification.Message.PublishTime }, { "MonitoredItem", monitoredItem?.ClientHandle }, { "SequenceNumber", notification.Message?.SequenceNumber }, }; TheOPCTag.LogOPCData(logInfo, MyOPCServer.GetLogAddress(), $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}"); } // ignore unknown events. if (NodeId.IsNull(eventTypeId)) { TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Unknown eventTypeId", eMsgLevel.l1_Error, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}: received {eventTypeId}")); return; } EventFilter filter = monitoredItem.Status.Filter as EventFilter; bool isRefreshEvent = false; var eventData = new Dictionary <string, object>(); DateTimeOffset sourceTime = DateTimeOffset.Now; string conditionId = null; bool? bRetain = null; int index = 0; foreach (var field in filter.SelectClauses) { var value = index < notification?.EventFields.Count ? notification?.EventFields[index].Value : null; if (value is ExtensionObject || value is ExtensionObject[]) { value = MyOPCServer.DecodeExtensionObjectToJson(value, out var ignored); } var name = field?.BrowsePath?.Count > 0 ? field.BrowsePath[0].Name : null; if (value is NodeId) { if (name == "EventType") { var eventType = value as NodeId; if (eventType == Opc.Ua.ObjectTypeIds.RefreshStartEventType) { TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.ESSENTIALS) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Received Refresh Start event", eMsgLevel.l4_Message, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}")); _currentConditionsByConditionId.Clear(); if (_bRefreshing) { // Two overlapping refresh starts received TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Received more than one Refresh Start event", eMsgLevel.l2_Warning, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}")); } _bRefreshing = true; _lastRefreshStartTime = DateTimeOffset.Now; isRefreshEvent = true; } else if (eventType == Opc.Ua.ObjectTypeIds.RefreshEndEventType) { TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.ESSENTIALS) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Received Refresh End event", eMsgLevel.l4_Message, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}")); if (!_bRefreshing) { TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Received Refresh End event without matching start event", eMsgLevel.l2_Warning, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}")); // refresh end received without refresh start } _bRefreshing = false; isRefreshEvent = true; if (EventInfo.AggregateRetainedConditions) { WriteAggregatedConditionsToProperty(tEventHost, TheCommonUtils.CDate(notification.Message.PublishTime)); } } } value = value.ToString(); } if (name == null) { if (field.TypeDefinitionId == Opc.Ua.ObjectTypeIds.BaseEventType) { name = "ConditionId"; } } if (name != null) { if (aliasMap.TryGetValue(index, out var alias)) { eventData[alias] = value; } else { eventData[name] = value; } switch (name) { case "ConditionId": conditionId = value?.ToString(); break; case "Retain": bRetain = TheCommonUtils.CBool(value); break; case "Time": sourceTime = TheCommonUtils.CDate(value); break; } } index++; } if (!isRefreshEvent) { var eventInfoProperties = EventInfo.GetPropertyNames(); var filteredEventProps = EventInfo.Properties?.Count > 0 ? eventData.Where(pk => !requiredEventFields.Contains(pk.Key) || eventInfoProperties.Contains(pk.Key)) //.Select(kv => //{ // if (aliasMap.TryGetValue(kv.Key, out var alias)) // { // return new KeyValuePair<string, object>(alias, kv.Value); // } // return kv; //}) .ToDictionary(kv => kv.Key, kv => kv.Value) : eventData; //if (_bRefreshing) //{ // filteredEventProps["Refresh"] = true; //} if (!EventInfo.AggregateRetainedConditions) { // Raw events // TODO avoid resending events due to a refresh? var eventAsJson = TheCommonUtils.SerializeObjectToJSONString(filteredEventProps); TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.VERBOSE) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Received raw event {eventAsJson}", eMsgLevel.l6_Debug, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}")); tEventHost.GetBaseThing().SetProperty(DisplayName, eventAsJson, TheCommonUtils.CDate(notification.Message.PublishTime)); } else { // Aggregated Condition State if (conditionId != null) { if (bRetain == false) { TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.ESSENTIALS) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Removed current event {conditionId}", eMsgLevel.l6_Debug, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}")); _currentConditionsByConditionId.RemoveNoCare(conditionId); } else { TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.ESSENTIALS) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Added current event {conditionId}", eMsgLevel.l6_Debug, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}")); _currentConditionsByConditionId[conditionId] = filteredEventProps; } if (!_bRefreshing) { WriteAggregatedConditionsToProperty(tEventHost, TheCommonUtils.CDate(notification.Message.PublishTime)); } } } // Legacy format: do we still need to support this? Existing event support was not really usable... //foreach (var eventField in eventData) //{ // string propertyName; // if (tEventHost is TheOPCUATagThing) // TODO Create a TheOPCUAEventThing // { // // If this is a dedicated event thing, use the original value name // // TODO Is this really what we want to do or do we also want to use the full browsepath for dedicated event things? // throw new NotImplementedException("Should never get here"); // //propertyName = field.BrowsePath[0].Name; // } // else // { // // This is an external, multi-event thing: use the browsepath to avoid collisions with properties from multiple events // propertyName = DisplayName + "." + eventField.Key; // } // SetPropertyFromVariant(tEventHost, propertyName, eventField.Value, sourceTime); //} } } catch (Exception ex) { TheBaseAssets.MySYSLOG.WriteToLog(78102, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyOPCServer.GetBaseThing().EngineName, $"Internal error processing event notification", eMsgLevel.l1_Error, $"{DisplayName} {GetNodeIdForLogs()} {EventInfo.AggregateRetainedConditions}: {ex.ToString()}")); } }