nilFM

milestone integration platform

2021-04-19

I've had the pleasure of working with the Milestone Integration Platform at work lately. While it is extremely powerful, and one of the big names in video surveillance software, this library is... how do I say it? Messy, convoluted, schizophrenic, and painful.

Let me walk you through some of the insanity...

A big part of the library is configuring the hardware, rules, users, alarms, etc. for your site. So there's the namespace: VideoOS.ConfigurationAPI

But wait, there's also this namespace which contains only one class! VideoOS.ConfigurationApi

And then there's the individual item we want to configure, represented by this class: VideoOS.ConfigurationAPI.ConfigurationItem

But it's really just a stub for the classes in this namespace: VideoOS.Platform.ConfigurationItems

And let's not even talk about this weird class: VideoOS.Platform.ConfigItem

So you have to use the ConfigurationAPI to get paths from ConfigurationItems and then use the ConfigurationItems.ItemTypeClass constructor to build the full representation of the configuration object by providing that path and the ServerId of the management server.

Or do you? You can ALSO use the EnvironmentManager.MasterSite.ServerId property to get an instance of a ManagementServer class and then go through, for example, var cameras = ManagementServer.CameraGroupFolder.CameraGroup.CameraFolder.Cameras instead of

var client = ClientProxyHelper.GetClientProxy();
var groupFolder = client.GetItem("/CameraGroupFolder");
var group = new CameraGroup(groupFolder.Path, EnvironmentManager.MasterSite.ServerId);
var cameras = group.CameraFolder.Cameras;

That's not even the half of it... Let's say I want to add an AlarmDefinition to an AlarmDefinitionFolder.

There's a class: VideoOS.Platform.ConfigurationItems.AlarmDefinition

But this is just to retreive one off the server.

If I want to create one, I've got to use this class: VideoOS.Platform.ConfigurationItems.AddAlarmDefinitionServerTask

But this class has no public constructor, inherited or otherwise -- the only way to create an instance of it is to call this: AddAlarmDefinitionServerTask VideoOS.ConfigurationItems.AlarmDefinitionFolder.AddAlarmDefinition()

So doing something like this I can get an actual instance of this wicked class:

var client = ClientProxyHelper.GetClientProxy();
var folderConfig = client.GetItem("/AlarmDefinitionFolder");
var alarmDefFolder = new VideoOS.Platform.ConfigurationItems.AlarmDefinitionFolder(
  EnvironmentManager.Instance.MasterSite.ServerId, folderConfig.Path);
var alarmDef =  alarmDefFolder.AddAlarmDefinition();

The first line above is using a class directly out of Milestone's examples. The second line uses the ConfigurationAPI to retrieve a ConfigurationItem. Then we use the Path in the configuration and the ServerId of our management server to retrieve the actual AlarmDefinitionFolder and use it to add a new alarm definition.

So now I've got a class where I can manipulate its fields to create the definition I'm after, and then call AddAlarmDefinitionServerTask.Execute() to tell the server to create it.

But some of these values refer to other objects in the site configuration, like cameras, event types, events, users, etc... And only some of these "multiple choice" values come with helpers to tell us what our valid choices are. For the rest, we have to go hunting for them.

If I take the example of the properties EventTypeGroup and EventType I have helpers EventTypeGroupValues and EventTypeValues that should tell me what my valid values are, and EventTypeValues should update depending on what EventTypeGroup is set to. That's how it works in the management client.

It turns out you've got to do something like this:

alarmDef.EventTypeGroup = alarmDef.EventTypeGroupValues.Select(x => x.Value).First();
alarmDef.ValidateItem();
// Now we should see the EventTypeValues are filled
Console.WriteLine(alarmDef.EventTypeValues.Count);

So, after getting familiar with how the library was structured and banging my head against the wall on a weekly basis or so, I was able to craft a WPF control that creates AlarmDefinitions, and without muchado from there, I was able to get said form to list available Alarms from the server and also delete and edit them.

Except...

When editing existing AlarmDefinitions, everything goes well -- set the dropdowns, add stuff to the multiselects, hit save -- Looks like success.

What..?

Refresh the alarm and... it didn't save...

For whatever reason, even if I log all the properties of the alarm right before saving and they all look as they should, I call AlarmDefinition.Save() and it comes back without exception, indicating a successful save, when I would reload the alarm it was apparent the save didn't go through.

I debugged this backwards and forwards off and on for a few weeks, and finally made headway by taking a step back and making an automated edit to a single alarm and saving it right when the window loaded. So I modified my saving routine to fetch a new copy of the alarm I'm editing and copy all the properties of the instance I edited with the form (and validated), and save that one.

I have no idea why it seems that calling AlarmDefinition.ValidateItem() causes AlarmDefinition.Save() to cease working.


After that, things were looking pretty good -- but while the draft pull request was waiting for the release cycle to catch up with it, I'd been testing the Alarm Editor and found a bug: where the Management Client shows GenericEvents as potential or actual sources of an alarm, my implementation didn't.

First, a bit of background on the types of objects that can be the sources of alarms. Among other things, they can be devices like Cameras, Servers, Relay Outputs, etc; They can be UserDefinedEvents which are fired based on rules or by other alarms, or they can be GenericEvents which are primitive network messages in plaintext matched against certain patterns (sort of the lowest common denominator to integrate with legacy hardware or other operating systems).

The SourceList property of an AlarmDefinition is a string which is a list of Paths separated by commas. A path looks like: ItemType[GUID] where ItemType is the type of item of course, and GUID is a 48-bit hexadecimal identifier in the UUID format (Microsoft and their followers call them GUIDs instead of UUIDs). What I found was that if I pulled an AlarmDefinition off the server with a GenericEvent in its SourceList, the Path for the GenericEvent didn't match the corresponding Path obtained by traversing the tree of ConfigurationItems. They aren't even in the same form. Where the one obtained from the tree is GenericEvent[GUID], the one in the AlarmDefinition.SourceList is UserDefinedEvent[GUID'], where GUID' != GUID

And what's weird is that in a situation where my Alarm Editor should show a GenericEvent as a potential source (eg, in a dropdown box), if I force load options using the canonical Path of form GenericEvent[GUID] the AlarmDefinition is rejected by the server!

The Management Client actually... conforms? to this behavior, as it lists the GenericEvents in the same group as the UserDefinedEvents. But I have two questions I need to resolve:

1. Given one of these weird Paths, how do I translate it into a human-readable name to put in the user-interface?
2. How do I get one of these funky paths in the form UserDefinedEvent[GUID'] for each GenericEvent?

The first question was rather easy to answer. I used the constructor VideoOS.Platform.ConfigurationItems.GenericEvent(serverId, path) and passed it the UserDefinedEvent[GUID'] path given to me by an AlarmDefinition that already had a GenericEvent as a source. It actually gave me a valid GenericEvent back, and all the properties checked out, despite its misleading Path.

The second question was a lot harder to figure out...

As I mentioned, traversing the tree of ConfigurationItems on the server doesn't yield anything with a Path that looks like UserDefinedEvent[GUID']. But there is at least on other way to probe the server for its precious goodies.

The function that yielded no results was VideoOS.ConfigurationApi.ClientService.ClientProxy.GetChildren(path) starting on the root path / and called recursively on every child until we have the whole tree.

There is another function from a completely different namespace: VideoOS.Platform.Configuration.GetItems() which gives the top-level item (the server), and you can call Item.GetChildren() in a similar fashion and get a complete tree that way.

The Item class has a property FQID which can also be passed into a constructor in the VideoOS.Platform.ConfigurationItems namespace. So my thought was to try VideoOS.Platform.ConfigurationItems.GenericEvent(Item.FQID) on all the Items and see what comes back valid... What is weird is that a LOT of things came back valid, with all kinds of Paths, but none of them actually real GenericEvents. When I did some diagnostic logging, I noticed there were a few at the end of the tree that never got properly constructed. And there were four... Which is the same number of GenericEvents as our test server has.

So I checked the Item.Properties and found out that Item.Properties["EventType"] == "Generic".

So OK, weird. So I whipped out the best debugger in the .NET world, try/catch/Console.WriteLine

try {
  GenericEvent g = new GenericEvent(i.FQID);
}
catch (Exception ex) {
  Console.WriteLine(ex);
}

Turns out it throws a PathNotFoundMIPException and the Exception.Message claims the Path in the form InputEvent[GUID'] is invalid.

The fact that this Path claims to belong to an InputEvent is really weird, but we have an amazing piece of information: GUID' is exactly the same GUID' that we are looking for! It turns out this GUID' is present in the FQID and we can construct a valid Path by some old fashioned string-building to get our desired UserDefinedEvent[GUID'].

So my new procedure is, when constructing my dropdown options for a SourceList, if the ValueTypeInfoList claims that a UserDefinedEvent is a valid source for our selected categories, I traverse the Item tree and check if the Item.Properties["EventType"] == "Generic", and if so I dig in the FQID for the GUID' therein and build the path UserDefinedEvent[GUID'], and pass that to the two-argument constructor that takes the ServerId and the Path.

I had thought for a little bit in my experiments that I might actually have to parse the Exception.Message to get the accursed GUID', but luckily the FQID happens to expose it directly.

I had posted on the Milestone developer forum regarding the incorrect item type in the FQID and they eventually got back to me saying to get the Paths for my GenericEvents like this:

private static string FindGenericEventPathFromFQID(FQID fqid)
{
  var genericEVentKind = new Guid("d4d19c01-03f2-4ac6-9d2b-5356b5de62f1");

  var geKind = new MIPKind(EnvironmentManager.Instance.MasterSite.ServerId, genericEVentKind);
  geKind.FillChildren(new string[] { "GenericEvent" });
  foreach (MIPItem item in geKind.MIPItemFolder.MIPItems)
  {
    if (fqid.ObjectId == new Guid(item.GetProperty("ShadowId")))
      return string.Format("GenericEvent[{0}]", item.Id);
  }
  return null;
}

I don't really see how this is any more straightforward than what I did... but for now we've got correct behavior for the feature... and I guess with all this mess that's more than we could ask for!