When writing AllJoyn consumer applications, the general approach is to have an XML Introspection definition for a certain device, generate a consumer/producer library and use that to discover and interact with the device.
There's plenty of applications showing how to do this, and the AllJoyn Visual Studio extension makes this easy. For instance see these resources:
Channel 9: Step-By-Step: Building AllJoyn Universal Windows Apps for Windows 10 Public Preview
Channel 9: Using the AllJoyn ® Studio Extension
If you are for instance writing an app to control AllJoyn enabled lights, you should be using this approach and generate code based on the AllJoyn Lighting Service Framework introspection xml.
However, if you want to write a generic AllJoyn device handler where you don't know the devices you'll be using up front, there's a different approach to discover and analyze all AllJoyn devices on your local network. This is what you need if you want to build for instance an at-runtime configurable home automation controller or an AllJoyn device explorer.
AllJoyn is based on D-BUS, so you could go low-level, but luckily there's a great but well-kept secret in the Microsoft IoT Samples repo that does all the hard work: The Device Providers library. You can find all the source code on GitHub right here:
https://github.com/ms-iot/samples/tree/develop/AllJoyn/Platform/DeviceProviders
This is the main library that drives the AllJoyn Explorer you can use to find and test all your AllJoyn devices. In fact it's so simple to use, that if you're willing to give up type-safety and many compile time errors, it's actually a little simpler to write a quick controller this way over getting the Introspection XML and generating the library needed to consume it.
Writing the first app
You can just download the source code, and add the project as a reference to your existing project. However, I've done the "hard" work and wrapped it all into a NuGet Package you can reference. I've suggested to the Microsoft AllJoyn guys to do this, but until (if?) that happens, you can use my unofficial package instead. I promise I'll try and keep it up to date with the GitHub source code.
First create a new blank Windows 10 Universal App project. Right-click the project in the solution explorer and select "Manage NuGet Packages...". Next search for "dotMorten.AllJoyn.DeviceProviders" (or part thereof), and hit the install package for the package.
DeviceProviders.AllJoynProvider p = new DeviceProviders.AllJoynProvider();
p.ServiceJoined += ServiceJoined;
p.ServiceDropped += ServiceDropped;
p.Start();
What this code does is start up listening for devices found or lost. Let's add the event handler code and output to the output window when a device is lost or found.
private void ServiceDropped(DeviceProviders.IProvider sender, DeviceProviders.ServiceDroppedEventArgs args)
{
var name = args.Service.AboutData?.DeviceName;
var id = args.Service.AboutData?.DeviceId;
System.Diagnostics.Debug.WriteLine($"Lost device '{name}' : ID = {id}");
}
private void ServiceJoined(DeviceProviders.IProvider sender, DeviceProviders.ServiceJoinedEventArgs args)
{
var name = args.Service.AboutData?.DeviceName;
var id = args.Service.AboutData?.DeviceId;
System.Diagnostics.Debug.WriteLine($"Found device '{name}' : ID = {id}");
}
The device is the "Service" property of the event argument. We're using the device's AboutData to get information about the device.
Interrogating the device members
Next we can query all the interfaces, and all its properties, methods and signals on the device using the following code:
foreach(var obj in args.Service.Objects)
{
foreach(var i in obj.Interfaces)
{
var properties = i.Properties;
var methods = i.Methods;
var events = i.Signals;
}
}
There's more information on each member as well. For instance you can check if a property is read-only, whether it signals when its value changes, what the type is, what the unit is etc.
Much of this information is available somewhat hidden as 'Annotations'. Here's the most typical checks you would do on annotations to learn more about a property or parameter:
//Listen for property changes if this property supports it
if (property.Annotations.ContainsKey("org.freedesktop.DBus.Property.EmitsChangedSignal") &&
property.Annotations["org.freedesktop.DBus.Property.EmitsChangedSignal"] == "true")
{
property.ValueChanged += Property_ValueChanged;
}
//Get the minimum value for this property
if (property.Annotations.ContainsKey("org.alljoyn.Bus.Type.Min"))
{
var minValue = property.Annotations["org.alljoyn.Bus.Type.Min"];
}
//Get the maximum value for this property
if (property.Annotations.ContainsKey("org.alljoyn.Bus.Type.Max"))
{
var maxValue = property.Annotations["org.alljoyn.Bus.Type.Max"];
}
//Get the property unit if specified
if (property.Annotations.ContainsKey("org.alljoyn.Bus.Type.Units"))
{
string unit = property.Annotations["org.alljoyn.Bus.Type.Units"];
}
Finding and invoking members
We can write various predicates to identify a device. For instance if we want to find any device that implements the lighting service framework:
bool isLamp = args.Service.ImplementsInterface("org.allseen.LSF.LampState");
But most commonly you want to find a specific device to execute a certain rule. You would use the Device ID for this.
bool myLightBulb = (id == "00:17:88:01:00:f8:de:f3-0b");
We can then find a property on an interface and change it. For instance to flip the light switch on a light bulb, we would write the following:
var bulb = args.Service;
var lampState = bulb.Objects.SelectMany(i=>i.Interfaces).
Where(i=>i.Name == "org.allseen.LSF.LampState").FirstOrDefault();
var onOffProperty = lampState.GetProperty("OnOff");
if (onOffProperty != null)
{
//Get the current state of the bulb
var onOffState = await onOffProperty.ReadValueAsync();
//Flip the switch
await onOffProperty.SetValueAsync(!(bool)onOffState.Value);
}
Monitoring device properties
Now that we can interrogate any device, let's write some code that tracks any property that signals its value changing, and display it in the app.
First lets add a panel in the XAML page where we can display the property readouts:
<ScrollViewer>
<StackPanel x:Name="propertiesPanel"/>
</ScrollViewer>
Next we'll add the code that goes through all properties on the devices as they are discovered and adding a text block for the property name and its value. We'll add this in the DeviceJoined event handler:
foreach (var obj in args.Service.Objects)
{
foreach (var i in obj.Interfaces)
{
var properties = i.Properties;
foreach (var property in properties)
{
if (property.Annotations.ContainsKey("org.freedesktop.DBus.Property.EmitsChangedSignal") &&
property.Annotations["org.freedesktop.DBus.Property.EmitsChangedSignal"] == "true")
{
var _ = Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
{
StackPanel panel = new StackPanel() { Orientation = Orientation.Horizontal };
panel.Children.Add(new TextBlock() { Text = $"{i.Name}.{property.Name} = ", FontWeight = Windows.UI.Text.FontWeights.Bold });
var tb = new TextBlock();
panel.Children.Add(tb);
propertiesPanel.Children.Add(panel);
property.ValueChanged += (s, a) => { Property_ValueChanged(s, a, tb); };
tb.Text = string.Format("{0}", (await property.ReadValueAsync()).Value);
});
}
}
}
}
And lastly the event handler for updating the textblock when the value changes:
private void Property_ValueChanged(IProperty sender, object args, TextBlock textBlock)
{
var _ = Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
textBlock.Text = string.Format("{0}", args);
});
}
Creating some rules
We now have all the building blocks to build some home automation with any AllJoyn device. It'll take some work to build a user interface to build customizable rules and run them, but we can create some simple hardcoded ones in code. My house has a SmartMeter for monitoring the entire hour's power consumption, that I'm reading every 5 seconds and exposing as an AllJoyn device. If my power consumption goes too high, I can set the color of a bulb to warn me of this. Here's what that might look like:
private async void SetupPowerTracking(DeviceProviders.IService powerMeter)
{
var meter = powerMeter.Objects.SelectMany(i=>i.Interfaces).
Where(i=>i.Name == "com.dotMorten.ZigBeeSEDeviceSystemBridge.PowerMeter").FirstOrDefault();
var usageProperty = meter.GetProperty("CurrentUsage");
usageProperty.ValueChanged += UsageProperty_ValueChanged;
var reading = await usageProperty.ReadValueAsync();
UsageProperty_ValueChanged(usageProperty, reading.Value);
}
private void UsageProperty_ValueChanged(DeviceProviders.IProperty sender, object args)
{
double powerUsage = (double)args;
System.Diagnostics.Debug.WriteLine($"Power consumption: {powerUsage}");
if (bulb != null) //If we have found the bulb earlier
{
var lampState = bulb.GetInterface("org.allseen.LSF.LampState");
var hueProperty = lampState.GetProperty("Hue");
hueProperty.SetValueAsync(0); //Set to Red
var saturationProperty = lampState.GetProperty("Saturation");
if(powerUsage > 1) //Using over 1kWh
{
saturationProperty.SetValueAsync(int.MaxValue); //Fully saturate to red
}
else
{
saturationProperty.SetValueAsync(0); //Desaturate to white
}
}
}
If you have a presence sensor with you, that detects when you come and go from your home (for instance a mobile phone app triggered with a geofence), you could write a different rule that turns on the front door light at night. That could look something like this:
private void SetupArrivalSensor(DeviceProviders.IService presenceSensor)
{
var sensor = presenceSensor.Objects.SelectMany(i => i.Interfaces).
Where(i => i.Name == "com.dotMorten.Location.Presence").FirstOrDefault();
var presenceProperty = sensor.GetProperty("IsPresent");
presenceProperty.ValueChanged += IsPresentProperty_ValueChanged;
}
private void IsPresentProperty_ValueChanged(DeviceProviders.IProperty sender, object args)
{
bool isPresent = (bool)args;
DateTime now =
DateTime.Now;
if(isPresent && now.Hour >= 17 || now.Hour < 5 && frontDoorLight != null) //If arrival between 5pm and 5am
{
var lampState = frontDoorLight.Objects.SelectMany(i=>i.Interfaces).
Where(i=>i.Name == "org.allseen.LSF.LampState").FirstOrDefault();
var onOffProperty = lampState.GetProperty("OnOff");
onOffProperty.SetValueAsync(true);
}
}
Conclusion
With the DeviceProviders library it's clear you can write pretty much any code against any device. While we hardcoded most things here, and you would normally use pre-generated libraries created from introspection xml to solve these specific scenarios, this article should now give you the tools to build a system where these values aren't hardcoded but discovered at runtime, and give the user the option of creating rules on the fly as new devices are added.
I've personally been working on such a project but it's taking me some time to get it to a sharable state, so I challenge you to beat me to it and build a user-friendly Home Automation Controller around AllJoyn! :-)
Comments