milestone integration platform
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...
getting into it
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 ConfigurationItem
s and then use the ConfigurationItems.SomeConfigItemClass
constructors 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);
editing alarms
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.
shadow GUID
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 GenericEvent
s 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 Camera
s, Server
s, Relay Output
s, etc; They can be UserDefinedEvent
s which are fired based on rules or by other alarms, or they can be GenericEvent
s 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 Path
s 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 ConfigurationItem
s. 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 GenericEvent
s in the same group as the UserDefinedEvent
s. But I have two questions I need to resolve:
- Given one of these weird
Path
s, how do I translate it into a human-readable name to put in the user-interface? - How do I get one of these funky paths in the form
UserDefinedEvent[GUID']
for eachGenericEvent
?
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 ConfigurationItem
s 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 Item
s and see what comes back valid... What is weird is that a LOT of things came back valid, with all kinds of Path
s, but none of them actually real GenericEvent
s. 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 GenericEvent
s 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 Path
s for my GenericEvent
s 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!