Now that we’ve seen a couple of posts showing how to query information about Hue lights via a Philips Hue bridge from Dynamo Studio, it’s time for the really fun stuff: controlling lights from Dynamo.
To make this happen some changes were needed to the zero-touch node that talks to the Philips Hue API on behalf of Dynamo, mainly to allow the setting of a light’s properties. The Philips Hue API allows this to be done either via Hue, Saturation and Brightness or by the XY offset in the CIE colour space. I ended up exposing the ability to set RGB values (which passes via CIE) or Hue, Saturation and Brightness directly. Both seem to work well enough, so the choice of which to use is left to you. The RGB approach won’t let you control brightness, so it may ultimately be beneficial to add a more direct control over that setting, in case you just want to dim (or undim) a light.
Anyway, here’s a look at the updated graph. You’ll see there’s a colour gradient control – with some UI controls allowing the user to set the start and end colour, although we could add as many colour-stops as we wanted – and the colours sent to the various lights will be at equal distances along this space, based on the number of lights that are connected to the bridge. You can also set the order of the lights manually, assuming you don’t want them to be coloured sequentially based on their ID.
As you can see from the number of spheres, the above graph was run in a room where we have a lot of Hue lights, to emphasize the effect. I think it’s pretty cool! Although the camera’s sensor doesn’t do the colour range justice, unfortunately.
Here’s a link to the Dynamo graph, and here’s the updated C# code for the zero-touch library:
using Autodesk.DesignScript.Runtime;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace DynamoHue
{
internal static class Extensions
{
internal static string[] GetNamesOfProperties(this JToken obj)
{
var names = new List<string>();
foreach (JProperty prop in obj)
{
names.Add(prop.Name);
}
return names.ToArray();
}
internal static string[] GetNameFieldOfProperties(this JToken obj)
{
return obj.GetFieldsOfProperties<string>("name").ToArray();
}
internal static List<T> GetFieldsOfProperties<T>(this JToken obj, string name)
{
var names = new List<T>();
foreach (JObject prop in obj.Values())
{
var jt = prop.GetValue(name);
names.Add(jt.ToObject<T>());
}
return names;
}
internal static async Task<string> GetUrl(this string uri)
{
using (var hc = new HttpClient())
{
// Get the response: if it's valid return the JSON string else null
var res = await hc.GetAsync(uri);
return
res.IsSuccessStatusCode ?
await res.Content.ReadAsStringAsync() :
null;
}
}
internal static async Task<string> PutUrl(this string uri, HttpContent cont)
{
using (var hc = new HttpClient())
{
// Get the response: if it's valid return the JSON string else null
var res = await hc.PutAsync(uri, cont);
return
res.IsSuccessStatusCode ?
await res.Content.ReadAsStringAsync() :
null;
}
}
internal static bool RgbFromCie(
double x, double y, out double r, out double g, out double b
)
{
// Convert the colour from CIE to RGB
var xy = new Colourful.xyYColor(x, y, y);
var converter = new Colourful.Conversion.ColourfulConverter();
var col = converter.ToRGB(xy);
r = col.R * 255;
g = col.G * 255;
b = col.B * 255;
return true;
}
internal static bool CieFromRgb(
double r, double g, double b, out double x, out double y
)
{
// Convert the colour from RGB to CIE
var rgb = new Colourful.RGBColor(r / 255, g / 255, b / 255);
var converter = new Colourful.Conversion.ColourfulConverter();
var col = converter.ToxyY(rgb);
x = col.x;
y = col.y;
return true;
}
}
public class Bridge
{
// The values for our IP address and bridge ID
private string _ip, _bridge;
internal Bridge(string ip, string bridge)
{
_ip = ip;
_bridge = bridge;
RefreshData();
}
public string[] LightIds { get; set; }
public string[] LightNames { get; set; }
public string[] GroupIds { get; set; }
public string[] GroupNames { get; set; }
public string[][] GroupLightIds { get; set; }
public string[] SceneIds { get; set; }
public string[] SceneNames { get; set; }
public string[] RuleIds { get; set; }
public string[] RuleNames { get; set; }
public string[] ScheduleIds { get; set; }
public string[] ScheduleNames { get; set; }
public string[] SensorIds { get; set; }
public string[] SensorNames { get; set; }
/// <summary>
/// Creates a bridge node based on an IP address and a bridge ID.
/// </summary>
/// <param name="ip">The IP address of the bridge</param>
/// <param name="bridge">The ID of the bridge</param>
/// <returns>A node representing the specified bridge</returns>
public static Bridge ByIpAndBridgeId(string ip, string bridge)
{
return new Bridge(ip, bridge);
}
/// <summary>
/// Refreshes the information for a bridge using the Philips Hue API
/// </summary>
/// <returns>Whether the API could find the specified bridge</returns>
[CanUpdatePeriodicallyAttribute(true)]
public Bridge RefreshData()
{
try
{
// Format the URL to return the sensor readings for a specified project
// and sensor
var url = String.Format("http://{0}/api/{1}", _ip, _bridge);
// Invoke our API with our credentials: returns null if auth expired
var res = url.GetUrl().Result;
// If we still haven't succeeded, return an invalid value
if (String.IsNullOrEmpty(res))
return null;
// Parse and sort the results
var jo = JObject.Parse(res);
var lights = jo["lights"];
var groups = jo["groups"];
var scenes = jo["scenes"];
var rules = jo["rules"];
var schedules = jo["schedules"];
var sensors = jo["sensors"];
if (lights == null || groups == null || scenes == null ||
rules == null || schedules == null || sensors == null)
return null;
LightIds = lights.GetNamesOfProperties();
LightNames = lights.GetNameFieldOfProperties();
GroupIds = groups.GetNamesOfProperties();
GroupNames = groups.GetNameFieldOfProperties();
GroupLightIds = groups.GetFieldsOfProperties<string[]>("lights").ToArray();
SceneIds = scenes.GetNamesOfProperties();
SceneNames = scenes.GetNameFieldOfProperties();
RuleIds = rules.GetNamesOfProperties();
RuleNames = rules.GetNameFieldOfProperties();
ScheduleIds = schedules.GetNamesOfProperties();
ScheduleNames = schedules.GetNameFieldOfProperties();
SensorIds = sensors.GetNamesOfProperties();
SensorNames = sensors.GetNameFieldOfProperties();
return this;
}
catch { return null; }
}
}
public class Light
{
// The values for our IP address, bridge and light IDs
private string _ip, _bridge, _light;
private bool _on;
private int _hue, _brightness, _saturation;
private double _x, _y, _r, _g, _b;
internal Light(string ip, string bridge, string light)
{
_ip = ip;
_bridge = bridge;
_light = light;
RefreshData();
}
public bool On { get; set; }
public int Hue { get { return _hue; } set { _hue = value; } }
public int Brightness { get { return _brightness; } set { _brightness = value; } }
public int Saturation { get { return _saturation; } set { _saturation = value; } }
public double R { get { return _r; } set { _r = value; } }
public double G { get { return _g; } set { _g = value; }}
public double B { get { return _b; } set { _b = value; }}
/// <summary>
/// Creates a light node based on an IP address, a bridge and light ID.
/// </summary>
/// <param name="ip">The IP address of the bridge</param>
/// <param name="bridge">The ID of the bridge</param>
/// <param name="light">The ID of the light</param>
/// <returns>A node representing the specified light</returns>
public static Light ByIpBridgeAndLightId(string ip, string bridge, string light)
{
return new Light(ip, bridge, light);
}
public void SetOn(bool on)
{
_on = on;
PushOnState();
}
public void SetRgb(double r, double g, double b)
{
_r = r;
_g = g;
_b = b;
PushRgbData();
}
public void SetHsb(double hue, double saturation, double brightness)
{
_hue = (int)Math.Floor(hue * 65535 / 360); // Returned in degrees!
_saturation = (int)Math.Floor(saturation * 254);
_brightness = (int)Math.Floor(brightness * 254);
PushHsbData();
}
/// <summary>
/// Refreshes the information for a light using the Philips Hue API
/// </summary>
/// <returns>The light itself</returns>
[CanUpdatePeriodicallyAttribute(true)]
public Light RefreshData()
{
try
{
// Format the URL to return the sensor readings for a specified project
// and sensor
var url =
String.Format("http://{0}/api/{1}/lights/{2}", _ip, _bridge, _light);
// Invoke our API with our credentials: returns null if auth expired
var res = url.GetUrl().Result;
// If we still haven't succeeded, return an invalid value
if (String.IsNullOrEmpty(res))
return null;
// Parse and sort the results
var jo = JObject.Parse(res);
var state = jo["state"];
if (state == null)
return null;
_on = (bool)state["on"];
_brightness = (int)state["bri"];
_hue = (int)state["hue"];
_saturation = (int)state["sat"];
_x = (double)state["xy"][0];
_y = (double)state["xy"][1];
Extensions.RgbFromCie(_x, _y, out _r, out _g, out _b);
return this;
}
catch { return null; }
}
/// <summary>
/// Pushes an "on/off" statea light using the Philips Hue API
/// </summary>
/// <returns>Success</returns>
internal bool PushOnState()
{
try
{
// Format the URL to return the sensor readings for a specified project
// and sensor
var url =
String.Format("http://{0}/api/{1}/lights/{2}/state", _ip, _bridge, _light);
var str = String.Format("{{\"on\": {0}}}", _on);
var httpContent = new StringContent(str.ToLower(), Encoding.UTF8, "application/json");
var res = url.PutUrl(httpContent).Result;
// If we haven't succeeded, return an invalid value
if (String.IsNullOrEmpty(res))
return false;
return true;
}
catch { return false; }
}
/// <summary>
/// Pushes RGB information to a light using the Philips Hue API
/// </summary>
/// <returns>Success</returns>
internal bool PushRgbData()
{
try
{
Extensions.CieFromRgb(_r, _g, _b, out _x, out _y);
// Format the URL to return the sensor readings for a specified project
// and sensor
var url =
String.Format("http://{0}/api/{1}/lights/{2}/state", _ip, _bridge, _light);
var str = String.Format("{{\"xy\": [{0},{1}]}}", _x, _y);
var httpContent = new StringContent(str, Encoding.UTF8, "application/json");
var res = url.PutUrl(httpContent).Result;
// If we haven't succeeded, return an invalid value
if (String.IsNullOrEmpty(res))
return false;
return true;
}
catch { return false; }
}
/// <summary>
/// Pushes HSL information to a light using the Philips Hue API
/// </summary>
/// <returns>Success</returns>
internal bool PushHsbData()
{
try
{
// Format the URL to return the sensor readings for a specified project
// and sensor
var url =
String.Format("http://{0}/api/{1}/lights/{2}/state", _ip, _bridge, _light);
var str =
String.Format(
"{{\"hue\": {0}, \"sat\": {1}, \"bri\": {2}}}",
_hue, _saturation, _brightness
);
var httpContent = new StringContent(str, Encoding.UTF8, "application/json");
var res = url.PutUrl(httpContent).Result;
// If we haven't succeeded, return an invalid value
if (String.IsNullOrEmpty(res))
return false;
return true;
}
catch { return false; }
}
}
}
Clearly the same technique could be used for much more involved scenarios… if you try it out and do something interesting, be sure to let me know!