diff --git a/.gitignore b/.gitignore
index 52aeb7a..96d3623 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
*.user
*.userosscache
*.sln.docstates
+.idea/
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
diff --git a/README.md b/README.md
index 594a0a2..1c4277d 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# LifxNet
-A .NET Standard 1.3 library for LIFX.
-Supports .NET, UWP, Xamarin iOS, Xamarin Android, and any other .NET Platform that has implemented .NET Standard 1.3+.
+A .NET Standard 2.0 library for LIFX.
+Supports .NET, UWP, Xamarin iOS, Xamarin Android, and any other .NET Platform that has implemented .NET Standard 2.0+.
For Cloud Protocol based implementation, check out [isaacrlevin's repo](https://github.com/isaacrlevin/LifxCloudClient)
@@ -22,14 +22,14 @@ PM> Install-Package LifxNet
Tested with LIFX 2.0 Firmware.
-Based on the official [LIFX protocol docs](https://github.com/LIFX/lifx-protocol-docs)
+Based on the official [LIFX protocol docs](https://lan.developer.lifx.com/docs)
####Usage
```csharp
- client = await LifxNet.LifxClient.CreateAsync();
- client.DeviceDiscovered += Client_DeviceDiscovered;
- client.DeviceLost += Client_DeviceLost;
+ client = new LifxClient();
+ client.Discovered += Client_DeviceDiscovered;
+ client.Lost += Client_DeviceLost;
client.StartDeviceDiscovery();
...
diff --git a/src/LifxNet.sln b/src/LifxNet.sln
index 49eedb8..7a700a2 100644
--- a/src/LifxNet.sln
+++ b/src/LifxNet.sln
@@ -17,6 +17,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApp.netcore", "Sample
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApp.Universal", "SampleApps\SampleApp.Universal\SampleApp.Universal.csproj", "{9072BB5D-AF63-4E60-98A6-86AE8CF898B1}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LifxEmulator", "SampleApps\LifxEmulator\LifxEmulator.csproj", "{35655608-57DD-43AB-9D2E-81B329CC6473}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorSendTest", "SampleApps\ColorSendTest\ColorSendTest.csproj", "{EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -83,6 +87,38 @@ Global
{9072BB5D-AF63-4E60-98A6-86AE8CF898B1}.Release|x86.ActiveCfg = Release|x86
{9072BB5D-AF63-4E60-98A6-86AE8CF898B1}.Release|x86.Build.0 = Release|x86
{9072BB5D-AF63-4E60-98A6-86AE8CF898B1}.Release|x86.Deploy.0 = Release|x86
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|ARM.Build.0 = Debug|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|x64.Build.0 = Debug|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|x86.Build.0 = Debug|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|Any CPU.Build.0 = Release|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|ARM.ActiveCfg = Release|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|ARM.Build.0 = Release|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|x64.ActiveCfg = Release|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|x64.Build.0 = Release|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|x86.ActiveCfg = Release|Any CPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|x86.Build.0 = Release|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|ARM.Build.0 = Debug|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|x64.Build.0 = Debug|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|x86.Build.0 = Debug|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|ARM.ActiveCfg = Release|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|ARM.Build.0 = Release|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|x64.ActiveCfg = Release|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|x64.Build.0 = Release|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|x86.ActiveCfg = Release|Any CPU
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -90,5 +126,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{9165A342-4844-4655-8D1E-D607CD3AA2BD} = {41199DCE-DF15-4BD2-B5A4-1836B3A5BF54}
{9072BB5D-AF63-4E60-98A6-86AE8CF898B1} = {41199DCE-DF15-4BD2-B5A4-1836B3A5BF54}
+ {35655608-57DD-43AB-9D2E-81B329CC6473} = {41199DCE-DF15-4BD2-B5A4-1836B3A5BF54}
+ {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7} = {41199DCE-DF15-4BD2-B5A4-1836B3A5BF54}
EndGlobalSection
EndGlobal
diff --git a/src/LifxNet/Color.cs b/src/LifxNet/Color.cs
deleted file mode 100644
index 569486b..0000000
--- a/src/LifxNet/Color.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace LifxNet
-{
- ///
- /// RGB Color structure
- ///
- public struct Color
- {
- ///
- /// Red
- ///
- public byte R { get; set; }
-
- ///
- /// Green
- ///
- public byte G { get; set; }
-
- ///
- /// Blue
- ///
- public byte B { get; set; }
- }
-}
\ No newline at end of file
diff --git a/src/LifxNet/Device.cs b/src/LifxNet/Device.cs
new file mode 100644
index 0000000..8f0fbf0
--- /dev/null
+++ b/src/LifxNet/Device.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Linq;
+
+namespace LifxNet {
+ ///
+ /// LIFX Generic Device
+ ///
+ public abstract class Device {
+ internal Device(string hostname, byte[] macAddress, byte service, uint port) {
+ if (hostname == null)
+ throw new ArgumentNullException(nameof(hostname));
+ if (string.IsNullOrWhiteSpace(hostname))
+ throw new ArgumentException(nameof(hostname));
+ HostName = hostname;
+ MacAddress = macAddress;
+ Service = service;
+ Port = port;
+ LastSeen = DateTime.MinValue;
+ }
+
+ ///
+ /// Hostname for the device
+ ///
+ public string HostName { get; internal set; }
+
+ ///
+ /// Service ID
+ ///
+ public byte Service { get; }
+
+ ///
+ /// Service port
+ ///
+ public uint Port { get; }
+
+ internal DateTime LastSeen { get; set; }
+
+ ///
+ /// Gets the MAC address
+ ///
+ public byte[] MacAddress { get; }
+
+ ///
+ /// Gets the MAC address
+ ///
+ public string MacAddressName {
+ get { return string.Join(":", MacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LifxNet/LifxClient.DeviceOperations.cs b/src/LifxNet/LifxClient.DeviceOperations.cs
index 4fe5b0b..70a139b 100644
--- a/src/LifxNet/LifxClient.DeviceOperations.cs
+++ b/src/LifxNet/LifxClient.DeviceOperations.cs
@@ -1,14 +1,9 @@
using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Threading;
+using System.Diagnostics;
using System.Threading.Tasks;
-namespace LifxNet
-{
- public partial class LifxClient : IDisposable
- {
+namespace LifxNet {
+ public partial class LifxClient {
///
/// Turns the device on
///
@@ -27,37 +22,28 @@ public partial class LifxClient : IDisposable
///
///
///
- public async Task SetDevicePowerStateAsync(Device device, bool isOn)
- {
+ public async Task SetDevicePowerStateAsync(Device device, bool isOn) {
if (device == null)
throw new ArgumentNullException(nameof(device));
- System.Diagnostics.Debug.WriteLine($"Sending DeviceSetPower({isOn}) to {device.HostName}");
- FrameHeader header = new FrameHeader()
- {
- Identifier = GetNextIdentifier(),
- AcknowledgeRequired = true
- };
+ Debug.WriteLine($"Sending DeviceSetPower({isOn}) to {device.HostName}");
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
_ = await BroadcastMessageAsync(device.HostName, header,
- MessageType.DeviceSetPower, (UInt16)(isOn ? 65535 : 0)).ConfigureAwait(false);
+ MessageType.DeviceSetPower, (ushort) (isOn ? 65535 : 0)).ConfigureAwait(false);
}
///
/// Gets the label for the device
///
///
- ///
- public async Task GetDeviceLabelAsync(Device device)
- {
+ /// The device label
+ public async Task GetDeviceLabelAsync(Device device) {
if (device == null)
throw new ArgumentNullException(nameof(device));
- FrameHeader header = new FrameHeader()
- {
- Identifier = GetNextIdentifier(),
- AcknowledgeRequired = false
- };
- var resp = await BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetLabel).ConfigureAwait(false);
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ var resp = await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceGetLabel).ConfigureAwait(false);
return resp.Label;
}
@@ -67,16 +53,11 @@ public async Task SetDevicePowerStateAsync(Device device, bool isOn)
///
///
///
- public async Task SetDeviceLabelAsync(Device device, string label)
- {
+ public async Task SetDeviceLabelAsync(Device device, string label) {
if (device == null)
throw new ArgumentNullException(nameof(device));
- FrameHeader header = new FrameHeader()
- {
- Identifier = GetNextIdentifier(),
- AcknowledgeRequired = true
- };
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
_ = await BroadcastMessageAsync(
device.HostName, header, MessageType.DeviceSetLabel, label).ConfigureAwait(false);
}
@@ -84,34 +65,212 @@ public async Task SetDeviceLabelAsync(Device device, string label)
///
/// Gets the device version
///
- public Task GetDeviceVersionAsync(Device device)
- {
+ public Task GetDeviceVersionAsync(Device device) {
if (device == null)
throw new ArgumentNullException(nameof(device));
- FrameHeader header = new FrameHeader()
- {
- Identifier = GetNextIdentifier(),
- AcknowledgeRequired = false
- };
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
return BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetVersion);
}
+
+ ///
+ /// Gets Host MCU firmware information.
+ ///
+ ///
+ ///
+ public Task GetDeviceHostFirmwareAsync(Device device) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceGetHostFirmware);
+ }
+
+ ///
+ /// Get Host MCU information.
+ ///
+ ///
+ ///
+ ///
+ public async Task GetHostInfoAsync(Device device) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceGetHostInfo);
+ }
+
+ ///
+ /// Get Host Wifi information.
+ ///
+ ///
+ ///
+ ///
+ public async Task GetWifiInfoAsync(Device device) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceGetWifiInfo);
+ }
+
+ ///
+ /// Get Host Wifi firmware information.
+ ///
+ ///
+ ///
+ ///
+ public async Task GetWifiFirmwareAsync(Device device) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceGetWifiFirmware);
+ }
+
+ ///
+ /// Get device power level
+ /// Zero implies standby and non-zero sets a corresponding power draw level. Currently only 0 and 65535 are supported.
+ ///
+ ///
+ /// 0 for off, 1 for on
+ ///
+ public async Task GetPowerAsync(Device device) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ var level = await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceGetPower);
+ return level.Level == 0 ? 0 : 1;
+ }
+
+ ///
+ /// Set Device power level.
+ /// Internally, Lifx offers a range from 0-65535, but actually only responds to 0 and 65535.
+ ///
+ ///
+ /// 0 for off, 1 for on
+ ///
+ ///
+ public async Task SetPowerAsync(Device device, int level) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
+ if (level != 0) level = 65535;
+ await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceSetPower, level);
+ }
+
+ ///
+ /// Get run-time information.
+ ///
+ ///
+ ///
+ ///
+ public async Task GetInfoAsync(Device device) {
+ if (device == null)
+ throw new ArrayTypeMismatchException(nameof(device));
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceGetInfo);
+ }
+
+ ///
+ /// Set the device location label
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task SetLocationAsync(Device device, string label) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
+ var rand = new Random();
+ var location = new byte[16];
+ rand.NextBytes(location);
+ var updated = DateTimeOffset.Now.ToUnixTimeSeconds();
+ await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceSetLocation, location, label, updated);
+ }
+
+ ///
+ /// Ask the device to return its location information.
+ ///
+ ///
+ ///
+ ///
+ public async Task GetLocationAsync(Device device) {
+ if (device == null)
+ throw new ArrayTypeMismatchException(nameof(device));
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceGetLocation);
+ }
+
///
- /// Gets the device's host firmware
+ /// Set the device group.
///
///
+ /// The new group name
///
- public Task GetDeviceHostFirmwareAsync(Device device)
- {
+ ///
+ public async Task SetGroupAsync(Device device, string label) {
if (device == null)
throw new ArgumentNullException(nameof(device));
- FrameHeader header = new FrameHeader()
- {
- Identifier = GetNextIdentifier(),
- AcknowledgeRequired = false
- };
- return BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetHostFirmware);
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
+ var rand = new Random();
+ var group = new byte[16];
+ rand.NextBytes(group);
+ var updated = DateTimeOffset.Now.ToUnixTimeSeconds();
+ await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceSetGroup, group, label, updated);
+ }
+
+ ///
+ /// Get the device group.
+ ///
+ ///
+ ///
+ ///
+ public async Task GetGroupAsync(Device device) {
+ if (device == null)
+ throw new ArrayTypeMismatchException(nameof(device));
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceGetGroup);
+ }
+
+ ///
+ /// Request an arbitrary payload be echoed back.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task RequestEcho(Device device, byte[] payload) {
+ if (device == null)
+ throw new ArrayTypeMismatchException(nameof(device));
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ // Truncate our input payload to be 64 bits exactly
+ var realPayload = new byte[64];
+ for (var i = 0; i < realPayload.Length; i++) {
+ if (i < payload.Length) {
+ realPayload[i] = payload[i];
+ } else {
+ realPayload[i] = 0;
+ }
+ }
+ return await BroadcastMessageAsync(device.HostName, header,
+ MessageType.DeviceEchoRequest, realPayload);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/LifxNet/LifxClient.Discovery.cs b/src/LifxNet/LifxClient.Discovery.cs
index 3f5b402..9576b1a 100644
--- a/src/LifxNet/LifxClient.Discovery.cs
+++ b/src/LifxNet/LifxClient.Discovery.cs
@@ -1,81 +1,88 @@
using System;
using System.Collections.Generic;
-using System.IO;
+using System.Diagnostics;
using System.Linq;
-using System.Text;
+using System.Net;
using System.Threading;
using System.Threading.Tasks;
-namespace LifxNet
-{
- public partial class LifxClient : IDisposable
- {
- private static uint identifier = 1;
- private static object identifierLock = new object();
- private UInt32 discoverSourceID;
- private CancellationTokenSource? _DiscoverCancellationSource;
- private Dictionary DiscoveredBulbs = new Dictionary();
-
- private static uint GetNextIdentifier()
- {
- lock (identifierLock)
- return identifier++;
+namespace LifxNet {
+ public partial class LifxClient {
+ private static uint _identifier = 1;
+ private static readonly object IdentifierLock = new object();
+ private uint _discoverSourceId;
+ private CancellationTokenSource? _discoverCancellationSource;
+ private readonly Dictionary _discoveredBulbs = new Dictionary();
+ private readonly int[] _stripIds = {31, 32, 38};
+ private readonly int[] _tileIds = {55};
+ private readonly int[] _switchIds = {70};
+
+ private static uint GetNextIdentifier() {
+ lock (IdentifierLock) {
+ _identifier++;
+ }
+
+ return _identifier;
}
///
/// Event fired when a LIFX bulb is discovered on the network
///
public event EventHandler? DeviceDiscovered;
+
///
/// Event fired when a LIFX bulb hasn't been seen on the network for a while (for more than 5 minutes)
///
public event EventHandler? DeviceLost;
private IList devices = new List();
-
+
///
/// Gets a list of currently known devices
///
- public IEnumerable Devices { get { return devices; } }
+ public IEnumerable Devices => devices;
///
- /// Event args for and events.
+ /// Event args for and events.
///
- public sealed class DeviceDiscoveryEventArgs : EventArgs
- {
+ public sealed class DeviceDiscoveryEventArgs : EventArgs {
internal DeviceDiscoveryEventArgs(Device device) => Device = device;
+
///
/// The device the event relates to
///
public Device Device { get; }
}
- private void ProcessDeviceDiscoveryMessage(System.Net.IPAddress remoteAddress, int remotePort, LifxResponse msg)
- {
- string id = msg.Header.TargetMacAddressName; //remoteAddress.ToString()
- if (DiscoveredBulbs.ContainsKey(id)) //already discovered
- {
- DiscoveredBulbs[id].LastSeen = DateTime.UtcNow; //Update datestamp
- DiscoveredBulbs[id].HostName = remoteAddress.ToString(); //Update hostname in case IP changed
-
- return;
+ private void ProcessDeviceDiscoveryMessage(IPAddress remoteAddress, LifxResponse msg) {
+ Debug.WriteLine("Processing device discovery message...");
+ string id = msg.Header.TargetMacAddressName; //remoteAddress.ToString()
+ if (_discoveredBulbs.ContainsKey(id)) //already discovered
+ {
+ _discoveredBulbs[id].LastSeen = DateTime.UtcNow; //Update datestamp
+ _discoveredBulbs[id].HostName = remoteAddress.ToString(); //Update hostname in case IP changed
+ Debug.WriteLine("Device already discovered, skipping.");
+ return;
}
- if (msg.Source != discoverSourceID || //did we request the discovery?
- _DiscoverCancellationSource == null ||
- _DiscoverCancellationSource.IsCancellationRequested) //did we cancel discovery?
+
+ if (msg.Source != _discoverSourceId || //did we request the discovery?
+ _discoverCancellationSource == null ||
+ _discoverCancellationSource.IsCancellationRequested) //did we cancel discovery?
return;
- var device = new LightBulb(remoteAddress.ToString(), msg.Header.TargetMacAddress, msg.Payload[0]
- , BitConverter.ToUInt32(msg.Payload, 1))
- {
- LastSeen = DateTime.UtcNow
+ var address = remoteAddress.ToString();
+ var mac = msg.Header.TargetMacAddress;
+ var svc = msg.Payload.GetUint8();
+ var port = msg.Payload.GetUInt32();
+ var lastSeen = DateTime.UtcNow;
+ Debug.WriteLine("Creating generic device: " + address + " and " + port);
+ var device = new LightBulb(address, mac, svc, port) {
+ LastSeen = lastSeen
};
- DiscoveredBulbs[id] = device;
+
+ _discoveredBulbs[id] = device;
devices.Add(device);
- if (DeviceDiscovered != null)
- {
- DeviceDiscovered(this, new DeviceDiscoveryEventArgs(device));
- }
+ DeviceDiscovered?.Invoke(this, new DeviceDiscoveryEventArgs(device));
}
///
@@ -84,123 +91,50 @@ private void ProcessDeviceDiscoveryMessage(System.Net.IPAddress remoteAddress, i
///
///
///
- public void StartDeviceDiscovery()
- {
- if (_DiscoverCancellationSource != null && !_DiscoverCancellationSource.IsCancellationRequested)
+ public void StartDeviceDiscovery() {
+ // Reset our list of devices on discovery start
+ devices = new List();
+ if (_discoverCancellationSource != null && !_discoverCancellationSource.IsCancellationRequested)
return;
- _DiscoverCancellationSource = new CancellationTokenSource();
- var token = _DiscoverCancellationSource.Token;
- var source = discoverSourceID = GetNextIdentifier();
+ _discoverCancellationSource = new CancellationTokenSource();
+ var token = _discoverCancellationSource.Token;
+ _discoverSourceId = GetNextIdentifier();
//Start discovery thread
- Task.Run(async () =>
- {
- System.Diagnostics.Debug.WriteLine("Sending GetServices");
- FrameHeader header = new FrameHeader()
- {
- Identifier = source
- };
- while (!token.IsCancellationRequested)
- {
- try
- {
- await BroadcastMessageAsync(null, header, MessageType.DeviceGetService);
+ Task.Run(async () => {
+ Debug.WriteLine("Sending GetServices...");
+ FrameHeader header = new FrameHeader(_discoverSourceId);
+ while (!token.IsCancellationRequested) {
+ try {
+ await BroadcastMessageAsync("255.255.255.255", header,
+ MessageType.DeviceGetService);
+ } catch (Exception e) {
+ Debug.WriteLine("Broadcast exception: " + e.Message);
}
- catch { }
- await Task.Delay(5000);
+
+ await Task.Delay(10000, token);
var lostDevices = devices.Where(d => (DateTime.UtcNow - d.LastSeen).TotalMinutes > 5).ToArray();
- if(lostDevices.Any())
- {
- foreach(var device in lostDevices)
- {
- devices.Remove(device);
- DiscoveredBulbs.Remove(device.MacAddressName);
- if (DeviceLost != null)
- DeviceLost(this, new DeviceDiscoveryEventArgs(device));
- }
+ if (!lostDevices.Any()) {
+ continue;
+ }
+
+ foreach (var device in lostDevices) {
+ devices.Remove(device);
+ _discoveredBulbs.Remove(device.MacAddressName);
+ DeviceLost?.Invoke(this, new DeviceDiscoveryEventArgs(device));
}
}
- });
+ }, token);
}
///
/// Stops device discovery
///
///
- public void StopDeviceDiscovery()
- {
- if (_DiscoverCancellationSource == null || _DiscoverCancellationSource.IsCancellationRequested)
+ public void StopDeviceDiscovery() {
+ if (_discoverCancellationSource == null || _discoverCancellationSource.IsCancellationRequested)
return;
- _DiscoverCancellationSource.Cancel();
- _DiscoverCancellationSource = null;
+ _discoverCancellationSource.Cancel();
+ _discoverCancellationSource = null;
}
}
-
- ///
- /// LIFX Generic Device
- ///
- public abstract class Device
- {
- internal Device(string hostname, byte[] macAddress, byte service, UInt32 port)
- {
- if (hostname == null)
- throw new ArgumentNullException(nameof(hostname));
- if(string.IsNullOrWhiteSpace(hostname))
- throw new ArgumentException(nameof(hostname));
- HostName = hostname;
- MacAddress = macAddress;
- Service = service;
- Port = port;
- LastSeen = DateTime.MinValue;
- }
-
- ///
- /// Hostname for the device
- ///
- public string HostName { get; internal set; }
-
- ///
- /// Service ID
- ///
- public byte Service { get; }
-
- ///
- /// Service port
- ///
- public UInt32 Port { get; }
-
- internal DateTime LastSeen { get; set; }
-
- ///
- /// Gets the MAC address
- ///
- public byte[] MacAddress { get; }
-
- ///
- /// Gets the MAC address
- ///
- public string MacAddressName
- {
- get
- {
- if (MacAddress == null) return string.Empty;
- return string.Join(":", MacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray());
- }
- }
- }
- ///
- /// LIFX light bulb
- ///
- public sealed class LightBulb : Device
- {
- ///
- /// Initializes a new instance of a bulb instead of relying on discovery. At least the host name must be provide for the device to be usable.
- ///
- /// Required
- ///
- ///
- ///
- public LightBulb(string hostname, byte[] macAddress, byte service = 0, UInt32 port = 0) : base(hostname, macAddress, service, port)
- {
- }
- }
-}
+}
\ No newline at end of file
diff --git a/src/LifxNet/LifxClient.LightOperations.cs b/src/LifxNet/LifxClient.LightOperations.cs
index 022895e..5639feb 100644
--- a/src/LifxNet/LifxClient.LightOperations.cs
+++ b/src/LifxNet/LifxClient.LightOperations.cs
@@ -1,15 +1,12 @@
using System;
using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Threading;
+using System.Diagnostics;
using System.Threading.Tasks;
-namespace LifxNet
-{
- public partial class LifxClient : IDisposable
- {
- private Dictionary> taskCompletions = new Dictionary>();
+namespace LifxNet {
+ public partial class LifxClient {
+ private readonly Dictionary> _taskCompletions =
+ new Dictionary>();
///
/// Turns a bulb on using the provided transition time
@@ -23,7 +20,8 @@ public partial class LifxClient : IDisposable
///
///
///
- public Task TurnBulbOnAsync(LightBulb bulb, TimeSpan transitionDuration) => SetLightPowerAsync(bulb, transitionDuration, true);
+ public Task TurnBulbOnAsync(LightBulb bulb, TimeSpan transitionDuration) =>
+ SetLightPowerAsync(bulb, transitionDuration, true);
///
/// Turns a bulb off using the provided transition time
@@ -34,7 +32,8 @@ public partial class LifxClient : IDisposable
///
///
///
- public Task TurnBulbOffAsync(LightBulb bulb, TimeSpan transitionDuration) => SetLightPowerAsync(bulb, transitionDuration, false);
+ public Task TurnBulbOffAsync(LightBulb bulb, TimeSpan transitionDuration) =>
+ SetLightPowerAsync(bulb, transitionDuration, false);
///
/// Turns a bulb on or off using the provided transition time
@@ -49,26 +48,22 @@ public partial class LifxClient : IDisposable
///
///
///
- public async Task SetLightPowerAsync(LightBulb bulb, TimeSpan transitionDuration, bool isOn)
- {
+ public async Task SetLightPowerAsync(LightBulb bulb, TimeSpan transitionDuration, bool isOn) {
if (bulb == null)
- throw new ArgumentNullException("bulb");
- if (transitionDuration.TotalMilliseconds > UInt32.MaxValue ||
- transitionDuration.Ticks < 0)
- throw new ArgumentOutOfRangeException("transitionDuration");
+ throw new ArgumentNullException(nameof(bulb));
+ if (transitionDuration.TotalMilliseconds > uint.MaxValue ||
+ transitionDuration.Ticks < 0)
+ throw new ArgumentOutOfRangeException(nameof(transitionDuration));
- FrameHeader header = new FrameHeader()
- {
- Identifier = GetNextIdentifier(),
- AcknowledgeRequired = true
- };
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
- var b = BitConverter.GetBytes((UInt16)transitionDuration.TotalMilliseconds);
+ var b = BitConverter.GetBytes((ushort) transitionDuration.TotalMilliseconds);
- System.Diagnostics.Debug.WriteLine($"Sending LightSetPower(on={isOn},duration={transitionDuration.TotalMilliseconds}ms) to {bulb.HostName}");
+ Debug.WriteLine(
+ $"Sending LightSetPower(on={isOn},duration={transitionDuration.TotalMilliseconds}ms) to {bulb.HostName}");
await BroadcastMessageAsync(bulb.HostName, header, MessageType.LightSetPower,
- (UInt16)(isOn ? 65535 : 0), b
+ (ushort) (isOn ? 65535 : 0), b
).ConfigureAwait(false);
}
@@ -77,42 +72,49 @@ await BroadcastMessageAsync(bulb.HostName, header, Mess
///
///
///
- public async Task GetLightPowerAsync(LightBulb bulb)
- {
+ public async Task GetLightPowerAsync(LightBulb bulb) {
if (bulb == null)
throw new ArgumentNullException(nameof(bulb));
- FrameHeader header = new FrameHeader()
- {
- Identifier = GetNextIdentifier(),
- AcknowledgeRequired = true
- };
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
return (await BroadcastMessageAsync(
bulb.HostName, header, MessageType.LightGetPower).ConfigureAwait(false)).IsOn;
}
+ ///
+ /// Sets color and temperature of bulb
+ ///
+ /// The bulb to set
+ /// The LifxColor to set the bulb to
+ /// An optional transition duration, in milliseconds.
+ ///
+ public Task SetColorAsync(LightBulb bulb, LifxColor color, int duration = 0) {
+ return SetColorAsync(bulb, (ushort)color.LifxHue, (ushort)color.LifxSaturation, (ushort)color.LifxBrightness, (ushort)color.K,
+ TimeSpan.FromMilliseconds(duration));
+ }
+
///
/// Sets color and temperature for a bulb
///
///
- ///
+ ///
///
///
- public Task SetColorAsync(LightBulb bulb, Color color, UInt16 kelvin) => SetColorAsync(bulb, color, kelvin, TimeSpan.Zero);
+ public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, ushort kelvin) =>
+ SetColorAsync(bulb, lifxColor, kelvin, TimeSpan.Zero);
///
/// Sets color and temperature for a bulb and uses a transition time to the provided state
///
///
- ///
+ ///
///
///
///
- public Task SetColorAsync(LightBulb bulb, Color color, UInt16 kelvin, TimeSpan transitionDuration)
- {
+ public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, ushort kelvin, TimeSpan transitionDuration) {
if (bulb == null)
throw new ArgumentNullException(nameof(bulb));
- var hsl = Utilities.RgbToHsl(color);
+ var hsl = Utilities.RgbToHsl(lifxColor);
return SetColorAsync(bulb, hsl[0], hsl[1], hsl[2], kelvin, transitionDuration);
}
@@ -127,80 +129,57 @@ public Task SetColorAsync(LightBulb bulb, Color color, UInt16 kelvin, TimeSpan t
///
///
public async Task SetColorAsync(LightBulb bulb,
- UInt16 hue,
- UInt16 saturation,
- UInt16 brightness,
- UInt16 kelvin,
- TimeSpan transitionDuration)
- {
+ ushort hue,
+ ushort saturation,
+ ushort brightness,
+ ushort kelvin,
+ TimeSpan transitionDuration) {
if (bulb == null)
throw new ArgumentNullException(nameof(bulb));
- if (transitionDuration.TotalMilliseconds > UInt32.MaxValue ||
- transitionDuration.Ticks < 0)
+ if (transitionDuration.TotalMilliseconds > uint.MaxValue ||
+ transitionDuration.Ticks < 0)
throw new ArgumentOutOfRangeException("transitionDuration");
- if (kelvin < 2500 || kelvin > 9000)
- {
+ if (kelvin < 2500 || kelvin > 9000) {
throw new ArgumentOutOfRangeException("kelvin", "Kelvin must be between 2500 and 9000");
}
- System.Diagnostics.Debug.WriteLine("Setting color to {0}", bulb.HostName);
- FrameHeader header = new FrameHeader()
- {
- Identifier = GetNextIdentifier(),
- AcknowledgeRequired = true
- };
- UInt32 duration = (UInt32)transitionDuration.TotalMilliseconds;
- var durationBytes = BitConverter.GetBytes(duration);
- var h = BitConverter.GetBytes(hue);
- var s = BitConverter.GetBytes(saturation);
- var b = BitConverter.GetBytes(brightness);
- var k = BitConverter.GetBytes(kelvin);
+ Debug.WriteLine("Setting color to {0}", bulb.HostName);
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
+ var duration = (uint) transitionDuration.TotalMilliseconds;
await BroadcastMessageAsync(bulb.HostName, header,
- MessageType.LightSetColor, (byte)0x00, //reserved
- hue, saturation, brightness, kelvin, //HSBK
- duration
+ MessageType.LightSetColor, (byte) 0x00, //reserved
+ hue, saturation, brightness, kelvin, //HSBK
+ duration
);
}
- /*
+
public async Task SetBrightnessAsync(LightBulb bulb,
- UInt16 brightness,
- TimeSpan transitionDuration)
- {
+ ushort brightness,
+ TimeSpan transitionDuration) {
if (transitionDuration.TotalMilliseconds > UInt32.MaxValue ||
- transitionDuration.Ticks < 0)
- throw new ArgumentOutOfRangeException("transitionDuration");
+ transitionDuration.Ticks < 0)
+ throw new ArgumentOutOfRangeException(nameof(transitionDuration));
- FrameHeader header = new FrameHeader()
- {
- Identifier = (uint)randomizer.Next(),
- AcknowledgeRequired = true
- };
- UInt32 duration = (UInt32)transitionDuration.TotalMilliseconds;
- var durationBytes = BitConverter.GetBytes(duration);
- var b = BitConverter.GetBytes(brightness);
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
+ var duration = (uint) transitionDuration.TotalMilliseconds;
await BroadcastMessageAsync(bulb.HostName, header,
MessageType.SetLightBrightness, brightness, duration
);
- }*/
-
- ///
- /// Gets the current state of the bulb
- ///
- ///
- ///
- public Task GetLightStateAsync(LightBulb bulb)
- {
+ }
+
+ ///
+ /// Gets the current state of the bulb
+ ///
+ ///
+ ///
+ public async Task GetLightStateAsync(LightBulb bulb) {
if (bulb == null)
throw new ArgumentNullException(nameof(bulb));
- FrameHeader header = new FrameHeader()
- {
- Identifier = GetNextIdentifier(),
- AcknowledgeRequired = false
- };
- return BroadcastMessageAsync(
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(
bulb.HostName, header, MessageType.LightGet);
}
@@ -210,17 +189,12 @@ public Task GetLightStateAsync(LightBulb bulb)
///
///
///
- public async Task GetInfraredAsync(LightBulb bulb)
- {
+ public async Task GetInfraredAsync(LightBulb bulb) {
if (bulb == null)
throw new ArgumentNullException(nameof(bulb));
- FrameHeader header = new FrameHeader()
- {
- Identifier = GetNextIdentifier(),
- AcknowledgeRequired = true
- };
- return (await BroadcastMessageAsync(
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return (await BroadcastMessageAsync(
bulb.HostName, header, MessageType.InfraredGet).ConfigureAwait(false)).Brightness;
}
@@ -230,19 +204,14 @@ public async Task GetInfraredAsync(LightBulb bulb)
///
///
///
- public async Task SetInfraredAsync(Device device, UInt16 brightness)
- {
+ public async Task SetInfraredAsync(Device device, ushort brightness) {
if (device == null)
throw new ArgumentNullException(nameof(device));
- System.Diagnostics.Debug.WriteLine($"Sending SetInfrared({brightness}) to {device.HostName}");
- FrameHeader header = new FrameHeader()
- {
- Identifier = GetNextIdentifier(),
- AcknowledgeRequired = true
- };
-
- _ = await BroadcastMessageAsync(device.HostName, header,
- MessageType.InfraredSet, (UInt16)brightness).ConfigureAwait(false);
+ Debug.WriteLine($"Sending SetInfrared({brightness}) to {device.HostName}");
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
+
+ await BroadcastMessageAsync(device.HostName, header,
+ MessageType.InfraredSet, brightness).ConfigureAwait(false);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/LifxNet/LifxClient.MultizoneOperations.cs b/src/LifxNet/LifxClient.MultizoneOperations.cs
new file mode 100644
index 0000000..4e13cb1
--- /dev/null
+++ b/src/LifxNet/LifxClient.MultizoneOperations.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LifxNet {
+ public partial class LifxClient : IDisposable {
+ ///
+ /// This message is used for changing the color of either a single or multiple zones.
+ ///
+ /// Target device
+ /// Start index to target
+ /// End index to target
+ /// LifxColor to use
+ /// How long to fade
+ /// Whether the effect should be applied immediately or not.
+ ///
+ ///
+ ///
+ public async Task SetColorZonesAsync(Device device, int startIndex, int endIndex, LifxColor color,
+ TimeSpan transitionDuration, bool apply=false) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+ if (transitionDuration.TotalMilliseconds > uint.MaxValue ||
+ transitionDuration.Ticks < 0) {
+ throw new ArgumentOutOfRangeException(nameof(transitionDuration));
+ }
+
+ if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex));
+ var doApply = apply ? 0x01 : 0x00;
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
+ var duration = (uint) transitionDuration.TotalMilliseconds;
+ await BroadcastMessageAsync(device.HostName, header,
+ MessageType.SetColorZones, (byte) startIndex, (byte) endIndex, color, duration, doApply);
+ }
+
+ ///
+ /// Set a zone of colors
+ ///
+ /// The device to set
+ /// Duration in ms
+ /// Start index of the zone. Should probably just be 0 for most cases.
+ /// An array of system.drawing.colors. For completeness, I should probably make an
+ /// overload for this that accepts HSB values, but that's kind of a pain. :P
+ /// Whether to apply the effect or immediately or not. defaults to false.
+ ///
+ /// Thrown if the device is null
+ /// Thrown if the duration is longer than the max
+ ///
+ public async Task SetExtendedColorZonesAsync(Device device, TimeSpan transitionDuration, uint index,
+ List colors, bool apply = false) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+ if (transitionDuration.TotalMilliseconds > uint.MaxValue ||
+ transitionDuration.Ticks < 0) {
+ throw new ArgumentOutOfRangeException(nameof(transitionDuration));
+ }
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier(), true);
+ var duration = (uint) transitionDuration.TotalMilliseconds;
+ var count = (byte) colors.Count;
+ var colorBytes = new List();
+ foreach (var color in colors) {
+ colorBytes.AddRange(color.ToBytes());
+ }
+ var doApply = apply ? 0x01 : 0x00;
+
+ await BroadcastMessageAsync(device.HostName, header,
+ MessageType.SetExtendedColorZones, duration, doApply, index, count, colorBytes);
+ }
+
+ ///
+ /// Try to get the color zones from our device.
+ ///
+ ///
+ ///
+ ///
+ public Task GetExtendedColorZonesAsync(Device device) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return BroadcastMessageAsync(
+ device.HostName, header, MessageType.GetExtendedColorZones);
+ }
+
+ ///
+ /// Try to get the color zones from our device, non-extended.
+ ///
+ /// Target device
+ /// Start index of requested zones
+ /// End index of requested zones
+ /// Either a "StateZone" response for single-zone devices, or "StateMultiZone" response.
+ ///
+ public Task GetColorZonesAsync(Device device, int startIndex, int endIndex) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+ if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex));
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return BroadcastMessageAsync(
+ device.HostName, header, MessageType.GetColorZones, (byte) startIndex, (byte) endIndex);
+ }
+
+ ///
+ /// Try to get the color zone from our device, non-extended.
+ ///
+ /// Target device
+ /// Selected index of the requested zone
+ /// Either a "StateZone" response for single-zone devices, or "StateMultiZone" response.
+ ///
+ public Task GetColorZoneAsync(Device device, int index) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return BroadcastMessageAsync(
+ device.HostName, header, MessageType.GetColorZones, (byte) index, (byte) index);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LifxNet/LifxClient.SwitchOperations.cs b/src/LifxNet/LifxClient.SwitchOperations.cs
new file mode 100644
index 0000000..b8174e0
--- /dev/null
+++ b/src/LifxNet/LifxClient.SwitchOperations.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Threading.Tasks;
+
+namespace LifxNet {
+ public partial class LifxClient {
+ ///
+ /// Get the power state of a relay on a switch device.
+ ///
+ ///
+ /// The relay on the switch starting from 0
+ /// A StateRelayPower message.
+ ///
+ public async Task GetRelayPowerAsync(Device device, int relayIndex) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(
+ device.HostName, header, MessageType.GetRelayPower, (byte) relayIndex);
+ }
+
+ ///
+ /// Set the power state of a relay on a switch device.
+ /// Current models of the LIFX switch do not have dimming capability,
+ /// so the two valid values are 0 for off and 65535 for on.
+ ///
+ ///
+ /// The relay on the switch starting from 0
+ /// Whether to turn the device on or not.
+ /// A StateRelayPower message.
+ ///
+ public async Task SetRelayPowerAsync(Device device, int relayIndex, bool enable) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+ var level = enable ? 65535 : 0;
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(
+ device.HostName, header, MessageType.SetRelayPower, (byte) relayIndex, (ushort) level);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LifxNet/LifxClient.TileOperations.cs b/src/LifxNet/LifxClient.TileOperations.cs
new file mode 100644
index 0000000..e905b2f
--- /dev/null
+++ b/src/LifxNet/LifxClient.TileOperations.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Threading.Tasks;
+
+namespace LifxNet {
+ public partial class LifxClient {
+ private const int Reserved = 0x00;
+
+ ///
+ /// This message returns information about the tiles in the chain.
+ ///
+ ///
+ /// StateDeviceChainResponse
+ public async Task GetDeviceChainAsync(Device group) {
+ if (group == null)
+ throw new ArgumentNullException(nameof(group));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(
+ group.HostName, header, MessageType.GetDeviceChain);
+ }
+
+ ///
+ /// Used to tell each tile what their position is.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task SetUserPositionAsync(Device group, int tileIndex, float userX, float userY) {
+ if (group == null)
+ throw new ArgumentNullException(nameof(group));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+
+ await BroadcastMessageAsync(group.HostName, header,
+ MessageType.SetUserPosition, tileIndex, Reserved, userX, userY).ConfigureAwait(false);
+ }
+
+ ///
+ /// Get the state of 64 pixels in the tile in a rectangle that has a starting point and width.
+ /// The tile_index is used to control the starting tile in the chain and length is used to get the state of
+ /// that many tiles beginning from the tile_index. This will result in a separate response from each tile.
+ ///
+ /// For the LIFX Tile it really only makes sense to set x and y to zero, and width to 8.
+ ///
+ ///
+ /// used to control the starting tile in the chain
+ /// used to get the state of that many tiles beginning from the tile_index.
+ /// Leave at 0
+ /// Leave at 0
+ /// Leave at 8
+ /// StateTileState64Response
+ public async Task GetTileState64Async(Device device, int tileIndex, int length,
+ int x = 0, int y = 0, int width = 8) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(
+ device.HostName, header, MessageType.GetTileState64, tileIndex, length, Reserved, x, y, width);
+ }
+
+ ///
+ /// Set the state of 64 pixels in the tile in a rectangle that has a starting point and width.
+ /// The tile_index is used to control the starting tile in the chain and length is used to get the state of
+ /// that many tiles beginning from the tile_index. This will result in a separate response from each tile.
+ ///
+ /// For the LIFX Tile it really only makes sense to set x and y to zero, and width to 8.
+ ///
+ ///
+ /// used to control the starting tile in the chain
+ /// used to get the state of that many tiles beginning from the tile_index.
+ ///
+ ///
+ /// Leave at 0
+ /// Leave at 0
+ /// Leave at 8
+ /// StateTileState64Response
+ public async Task SetTileState64Async(Device device, int tileIndex, int length,
+ long duration, LifxColor[] colors, int x = 0, int y = 0, int width = 8) {
+ if (device == null)
+ throw new ArgumentNullException(nameof(device));
+
+ FrameHeader header = new FrameHeader(GetNextIdentifier());
+ return await BroadcastMessageAsync(
+ device.HostName, header, MessageType.SetTileState64, tileIndex, length, Reserved, x, y, width, duration,
+ colors);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LifxNet/LifxClient.cs b/src/LifxNet/LifxClient.cs
index 6ed088e..85963dc 100644
--- a/src/LifxNet/LifxClient.cs
+++ b/src/LifxNet/LifxClient.cs
@@ -1,300 +1,253 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
+using System.Diagnostics;
using System.IO;
-using System.Text;
-using System.Threading.Tasks;
-using System.Net.Sockets;
+using System.Linq;
using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
-namespace LifxNet
-{
+namespace LifxNet {
///
/// LIFX Client for communicating with bulbs
///
- public partial class LifxClient : IDisposable
- {
-
+ public partial class LifxClient {
private const int Port = 56700;
- private UdpClient? _socket;
- private bool _isRunning;
+ private readonly UdpClient _socket;
+ private bool _isRunning;
- private LifxClient()
- {
+ private LifxClient() {
+ IPEndPoint end = new IPEndPoint(IPAddress.Any, Port);
+ _socket = new UdpClient(end) {Client = {Blocking = false}, DontFragment = true};
+ _socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
///
/// Creates a new LIFX client.
///
/// client
- public static Task CreateAsync()
- {
+ public static Task CreateAsync() {
LifxClient client = new LifxClient();
client.Initialize();
- return Task.FromResult(client);
+ return Task.FromResult(client);
}
- private void Initialize()
- {
- IPEndPoint end = new IPEndPoint(IPAddress.Any, Port);
- _socket = new UdpClient(end);
- _socket.Client.Blocking = false;
- _socket.DontFragment = true;
- _socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- _isRunning = true;
- StartReceiveLoop();
+ private void Initialize() {
+ _isRunning = true;
+ StartReceiveLoop();
}
- private void StartReceiveLoop()
- {
- Task.Run(async () =>
- {
- while (_isRunning && _socket != null)
- try
- {
- var result = await _socket.ReceiveAsync();
- if (result.Buffer.Length > 0)
- {
- HandleIncomingMessages(result.Buffer, result.RemoteEndPoint);
- }
- }
- catch { }
- });
- }
-
- private void HandleIncomingMessages(byte[] data, System.Net.IPEndPoint endpoint)
- {
+
+ private void StartReceiveLoop() {
+ Task.Run(async () => {
+ while (_isRunning)
+ try {
+ var result = await _socket.ReceiveAsync();
+ if (result.Buffer.Length > 0) {
+ HandleIncomingMessages(result.Buffer, result.RemoteEndPoint);
+ }
+ } catch {
+ // ignored
+ }
+ });
+ }
+
+ private void HandleIncomingMessages(byte[] data, IPEndPoint endpoint) {
var remote = endpoint;
var msg = ParseMessage(data);
- if (msg.Type == MessageType.DeviceStateService)
- {
- ProcessDeviceDiscoveryMessage(remote.Address, remote.Port, msg);
- }
- else
- {
- if (taskCompletions.ContainsKey(msg.Source))
- {
- var tcs = taskCompletions[msg.Source];
- tcs(msg);
- }
- else
- {
- //TODO
- }
+ if (remote.Port == 56700) Debug.WriteLine("Incoming Message Type: " + msg.Type);
+ switch (msg.Type) {
+ case MessageType.DeviceStateService:
+ ProcessDeviceDiscoveryMessage(remote.Address, msg);
+ break;
+ default:
+ if (_taskCompletions.ContainsKey(msg.Source)) {
+ var tcs = _taskCompletions[msg.Source];
+ tcs(msg);
+ }
+
+ break;
}
- System.Diagnostics.Debug.WriteLine("Received from {0}:{1}", remote.ToString(),
- string.Join(",", (from a in data select a.ToString("X2")).ToArray()));
+ if (remote.Port == 56700)
+ Debug.WriteLine("Received from {0}:{1}", remote,
+ string.Join(",", (from a in data select a.ToString("X2")).ToArray()));
}
///
/// Disposes the client
///
- public void Dispose()
- {
- _isRunning = false;
- _socket?.Dispose();
+ public void Dispose() {
+ _isRunning = false;
+ _socket.Dispose();
}
- private Task BroadcastMessageAsync(string? hostName, FrameHeader header, MessageType type, params object[] args)
- where T : LifxResponse
-
- {
- List payload = new List();
- if (args != null)
- {
- foreach (var arg in args)
- {
- if (arg is UInt16)
- payload.AddRange(BitConverter.GetBytes((UInt16)arg));
- else if (arg is UInt32)
- payload.AddRange(BitConverter.GetBytes((UInt32)arg));
- else if (arg is byte)
- payload.Add((byte)arg);
- else if (arg is byte[])
- payload.AddRange((byte[])arg);
- else if (arg is string)
- payload.AddRange(Encoding.UTF8.GetBytes(((string)arg).PadRight(32).Take(32).ToArray())); //All strings are 32 bytes
- else
- throw new NotSupportedException(args.GetType().FullName);
- }
- }
- return BroadcastMessagePayloadAsync(hostName, header, type, payload.ToArray());
+ private Task BroadcastMessageAsync(string hostName, FrameHeader header, MessageType type,
+ params object[] args)
+ where T : LifxResponse {
+ Debug.WriteLine("Broadcasting " + type + " to " + hostName);
+ var payload = new Payload(args);
+
+ return BroadcastPayloadAsync(hostName, header, type, payload);
}
- private async Task BroadcastMessagePayloadAsync(string? hostName, FrameHeader header, MessageType type, byte[] payload)
- where T : LifxResponse
- {
+
+ private async Task BroadcastPayloadAsync(string hostName, FrameHeader header, MessageType type,
+ Payload payload)
+ where T : LifxResponse {
if (_socket == null)
throw new InvalidOperationException("No valid socket");
-#if DEBUG
- // MemoryStream ms = new MemoryStream();
- // await WritePacketToStreamAsync(ms.AsOutputStream(), header, (UInt16)type, payload).ConfigureAwait(false);
- // var data = ms.ToArray();
- // System.Diagnostics.Debug.WriteLine(
- // string.Join(",", (from a in data select a.ToString("X2")).ToArray()));
-#endif
- if (hostName == null)
- {
- hostName = "255.255.255.255";
- }
+
+ MemoryStream ms = new MemoryStream();
+ WritePacketToStream(ms, header, (UInt16) type, payload);
+ var data = ms.ToArray();
+ Debug.WriteLine(
+ string.Join(",", (from a in data select a.ToString("X2")).ToArray()));
+
+
TaskCompletionSource? tcs = null;
- if (//header.AcknowledgeRequired &&
- header.Identifier > 0 &&
- typeof(T) != typeof(UnknownResponse))
- {
+ if ( //header.AcknowledgeRequired &&
+ header.Identifier > 0 &&
+ typeof(T) != typeof(UnknownResponse)) {
tcs = new TaskCompletionSource();
- Action action = (r) =>
- {
+ Action action = (r) => {
if (r.GetType() == typeof(T))
- tcs.TrySetResult((T)r);
+ tcs.TrySetResult((T) r);
};
- taskCompletions[header.Identifier] = action;
+ _taskCompletions[header.Identifier] = action;
+ }
+
+ using (MemoryStream stream = new MemoryStream()) {
+ WritePacketToStream(stream, header, (UInt16) type, payload);
+ var msg = stream.ToArray();
+ await _socket.SendAsync(msg, msg.Length, hostName, Port);
}
- using (MemoryStream stream = new MemoryStream())
- {
- WritePacketToStream(stream, header, (UInt16)type, payload);
- var msg = stream.ToArray();
- await _socket.SendAsync(msg, msg.Length, hostName, Port);
- }
//{
// await WritePacketToStreamAsync(stream, header, (UInt16)type, payload).ConfigureAwait(false);
//}
T result = default(T);
- if(tcs != null)
- {
- var _ = Task.Delay(1000).ContinueWith((t) =>
- {
+ if (tcs != null) {
+ var _ = Task.Delay(1000).ContinueWith((t) => {
if (!t.IsCompleted)
tcs.TrySetException(new TimeoutException());
});
try {
result = await tcs.Task.ConfigureAwait(false);
- }
- finally
- {
- taskCompletions.Remove(header.Identifier);
+ } finally {
+ _taskCompletions.Remove(header.Identifier);
}
}
+
return result;
}
- private LifxResponse ParseMessage(byte[] packet)
- {
- using (MemoryStream ms = new MemoryStream(packet))
- {
- var header = new FrameHeader();
- BinaryReader br = new BinaryReader(ms);
- //frame
- var size = br.ReadUInt16();
- if (packet.Length != size || size < 36)
- throw new Exception("Invalid packet");
- var a = br.ReadUInt16(); //origin:2, reserved:1, addressable:1, protocol:12
- var source = br.ReadUInt32();
- //frame address
- byte[] target = br.ReadBytes(8);
- header.TargetMacAddress = target;
- ms.Seek(6, SeekOrigin.Current); //skip reserved
- var b = br.ReadByte(); //reserved:6, ack_required:1, res_required:1,
- header.Sequence = br.ReadByte();
- //protocol header
- var nanoseconds = br.ReadUInt64();
- header.AtTime = Utilities.Epoch.AddMilliseconds(nanoseconds * 0.000001);
- var type = (MessageType)br.ReadUInt16();
- ms.Seek(2, SeekOrigin.Current); //skip reserved
- return LifxResponse.Create(header, type, source, size > 36 ? br.ReadBytes(size - 36) : new byte[] { });
- }
+ private static LifxResponse ParseMessage(byte[] packet) {
+ using MemoryStream ms = new MemoryStream(packet);
+ var header = new FrameHeader();
+ BinaryReader br = new BinaryReader(ms);
+ //frame
+ var size = br.ReadUInt16();
+ if (packet.Length != size || size < 36)
+ throw new Exception("Invalid packet");
+ br.ReadUInt16(); //origin:2, reserved:1, addressable:1, protocol:12
+ var source = br.ReadUInt32();
+ //frame address
+ byte[] target = br.ReadBytes(8);
+ header.TargetMacAddress = target;
+ ms.Seek(6, SeekOrigin.Current); //skip reserved
+ br.ReadByte(); //reserved:6, ack_required:1, res_required:1,
+ header.Sequence = br.ReadByte();
+ //protocol header
+ var nanoseconds = br.ReadUInt64();
+ header.AtTime = Utilities.Epoch.AddMilliseconds(nanoseconds * 0.000001);
+ var type = (MessageType) br.ReadUInt16();
+ ms.Seek(2, SeekOrigin.Current); //skip reserved
+ return LifxResponse.Create(header, type, source,
+ new Payload(size > 36 ? br.ReadBytes(size - 36) : new byte[] { }));
}
- private void WritePacketToStream(Stream outStream, FrameHeader header, UInt16 type, byte[] payload)
- {
- using (var dw = new BinaryWriter(outStream) { /*ByteOrder = ByteOrder.LittleEndian*/ })
- {
- //BinaryWriter bw = new BinaryWriter(ms);
- #region Frame
- //size uint16
- dw.Write((UInt16)((payload != null ? payload.Length : 0) + 36)); //length
- // origin (2 bits, must be 0), reserved (1 bit, must be 0), addressable (1 bit, must be 1), protocol 12 bits must be 0x400) = 0x1400
- dw.Write((UInt16)0x3400); //protocol
- dw.Write((UInt32)header.Identifier); //source identifier - unique value set by the client, used by responses. If 0, responses are broadcasted instead
- #endregion Frame
-
- #region Frame address
- //The target device address is 8 bytes long, when using the 6 byte MAC address then left -
- //justify the value and zero-fill the last two bytes. A target device address of all zeroes effectively addresses all devices on the local network
- dw.Write(header.TargetMacAddress); // target mac address - 0 means all devices
- dw.Write(new byte[] { 0, 0, 0, 0, 0, 0 }); //reserved 1
-
- //The client can use acknowledgements to determine that the LIFX device has received a message.
- //However, when using acknowledgements to ensure reliability in an over-burdened lossy network ...
- //causing additional network packets may make the problem worse.
- //Client that don't need to track the updated state of a LIFX device can choose not to request a
- //response, which will reduce the network burden and may provide some performance advantage. In
- //some cases, a device may choose to send a state update response independent of whether res_required is set.
- if (header.AcknowledgeRequired && header.ResponseRequired)
- dw.Write((byte)0x03);
- else if (header.AcknowledgeRequired)
- dw.Write((byte)0x02);
- else if (header.ResponseRequired)
- dw.Write((byte)0x01);
- else
- dw.Write((byte)0x00);
- //The sequence number allows the client to provide a unique value, which will be included by the LIFX
- //device in any message that is sent in response to a message sent by the client. This allows the client
- //to distinguish between different messages sent with the same source identifier in the Frame. See
- //ack_required and res_required fields in the Frame Address.
- dw.Write((byte)header.Sequence);
- #endregion Frame address
-
- #region Protocol Header
- //The at_time value should be zero for Set and Get messages sent by a client.
- //For State messages sent by a device, the at_time will either be the device
- //current time when the message was received or zero. StateColor is an example
- //of a message that will return a non-zero at_time value
- if (header.AtTime > DateTime.MinValue)
- {
- var time = header.AtTime.ToUniversalTime();
- dw.Write((UInt64)(time - new DateTime(1970, 01, 01)).TotalMilliseconds * 10); //timestamp
- }
- else
- {
- dw.Write((UInt64)0);
- }
- #endregion Protocol Header
- dw.Write(type); //packet _type
- dw.Write((UInt16)0); //reserved
- if (payload != null)
- dw.Write(payload);
- dw.Flush();
+ private static void WritePacketToStream(Stream outStream, FrameHeader header, ushort type, Payload payload) {
+ using var dw = new BinaryWriter(outStream);
+
+ #region Frame
+
+ //size uint16
+ dw.Write((ushort) (payload.Length + 36)); //length
+ // origin (2 bits, must be 0), reserved (1 bit, must be 0), addressable (1 bit, must be 1), protocol 12 bits must be 0x400) = 0x1400
+ dw.Write((ushort) 0x3400); //protocol
+ dw.Write(header
+ .Identifier); //source identifier - unique value set by the client, used by responses. If 0, responses are broadcast instead
+
+ #endregion Frame
+
+ #region Frame address
+
+ //The target device address is 8 bytes long, when using the 6 byte MAC address then left -
+ //justify the value and zero-fill the last two bytes. A target device address of all zeroes effectively addresses all devices on the local network
+ dw.Write(header.TargetMacAddress); // target mac address - 0 means all devices
+ dw.Write(new byte[] {0, 0, 0, 0, 0, 0}); //reserved 1
+
+ //The client can use acknowledgements to determine that the LIFX device has received a message.
+ //However, when using acknowledgements to ensure reliability in an over-burdened lossy network ...
+ //causing additional network packets may make the problem worse.
+ //Client that don't need to track the updated state of a LIFX device can choose not to request a
+ //response, which will reduce the network burden and may provide some performance advantage. In
+ //some cases, a device may choose to send a state update response independent of whether res_required is set.
+ if (header.AcknowledgeRequired && header.ResponseRequired)
+ dw.Write((byte) 0x03);
+ else if (header.AcknowledgeRequired)
+ dw.Write((byte) 0x02);
+ else if (header.ResponseRequired)
+ dw.Write((byte) 0x01);
+ else
+ dw.Write((byte) 0x00);
+ //The sequence number allows the client to provide a unique value, which will be included by the LIFX
+ //device in any message that is sent in response to a message sent by the client. This allows the client
+ //to distinguish between different messages sent with the same source identifier in the Frame. See
+ //ack_required and res_required fields in the Frame Address.
+ dw.Write(header.Sequence);
+
+ #endregion Frame address
+
+ #region Protocol Header
+
+ //The at_time value should be zero for Set and Get messages sent by a client.
+ //For State messages sent by a device, the at_time will either be the device
+ //current time when the message was received or zero. StateColor is an example
+ //of a message that will return a non-zero at_time value
+ if (header.AtTime > DateTime.MinValue) {
+ var time = header.AtTime.ToUniversalTime();
+ dw.Write((ulong) (time - new DateTime(1970, 01, 01)).TotalMilliseconds * 10); //timestamp
+ } else {
+ dw.Write((ulong) 0);
}
+
+ #endregion Protocol Header
+
+ dw.Write(type); //packet _type
+ dw.Write((ushort) 0); //reserved
+ dw.Write(payload.ToArray());
+ dw.Flush();
}
}
- internal class FrameHeader
- {
- public UInt32 Identifier;
+ internal class FrameHeader {
+ public uint Identifier;
public byte Sequence;
public bool AcknowledgeRequired;
public bool ResponseRequired;
- public byte[] TargetMacAddress;
- public DateTime AtTime;
- public FrameHeader()
- {
- Identifier = 0;
- Sequence = 0;
- AcknowledgeRequired = false;
- ResponseRequired = false;
- TargetMacAddress = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
- AtTime = DateTime.MinValue;
+ public byte[] TargetMacAddress = {0, 0, 0, 0, 0, 0, 0, 0};
+ public DateTime AtTime = DateTime.MinValue;
+
+ public FrameHeader() {
}
- public string TargetMacAddressName
- {
- get
- {
- if (TargetMacAddress == null) return string.Empty;
- return string.Join(":", TargetMacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray());
- }
- }
- }
-
-}
+
+ public FrameHeader(uint id, bool acknowledgeRequired = false) {
+ Identifier = id;
+ AcknowledgeRequired = acknowledgeRequired;
+ }
+
+ public string TargetMacAddressName {
+ get { return string.Join(":", TargetMacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LifxNet/LifxColor.cs b/src/LifxNet/LifxColor.cs
new file mode 100644
index 0000000..3783f3a
--- /dev/null
+++ b/src/LifxNet/LifxColor.cs
@@ -0,0 +1,251 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Globalization;
+
+namespace LifxNet {
+ ///
+ /// Extend the normal System.Drawing.Color class and make it work with HSBK
+ ///
+ public class LifxColor {
+ private static double Tolerance
+ => 0.000000000000001;
+
+ private Color _color;
+
+
+ ///
+ /// Red
+ ///
+ public byte R {
+ get => _color.R;
+ set => _color = Color.FromArgb(_color.A, value, _color.G, _color.B);
+ }
+
+ ///
+ /// Green
+ ///
+ public byte G {
+ get => _color.G;
+ set => _color = Color.FromArgb(_color.A, _color.R, value, _color.B);
+ }
+
+ ///
+ /// Blue
+ ///
+ public byte B {
+ get => _color.B;
+ set => _color = Color.FromArgb(_color.A, _color.R, _color.G, value);
+ }
+
+ ///
+ /// The hue, in degrees, of this Color. The hue is measured in degrees, ranging from 0.0 through 360.0, in HSL color space.
+ ///
+ public float Hue {
+ get => _color.GetHue();
+ set => _color = HsbToRgb(value, _color.GetSaturation(), _color.GetBrightness());
+ }
+
+ ///
+ /// The hue, in the standard Lifx format, of this Color. The lifx hue is measured from 0 - 65565.
+ ///
+ public int LifxHue => (int) _color.GetHue() / 360 * 65536;
+
+ ///
+ /// The saturation of this Color. The saturation ranges from 0.0 through 1.0, where 0.0 is grayscale and 1.0 is the most saturated.
+ ///
+ public float Saturation {
+ get => _color.GetSaturation();
+ set => _color = HsbToRgb(_color.GetHue(), value, _color.GetBrightness());
+ }
+
+ ///
+ /// The saturation of this color in Lifx format. Range is 0 - 65535;
+ ///
+ public int LifxSaturation => (int) _color.GetSaturation() * 65535;
+
+ ///
+ /// The lightness of this Color. The lightness ranges from 0.0 through 1.0, where 0.0 represents black and 1.0 represents white.
+ ///
+ public float Brightness {
+ get => _color.GetBrightness();
+ set => _color = HsbToRgb(_color.GetHue(), _color.GetSaturation(), value);
+ }
+
+ ///
+ /// The brightness of this color. The brightness range is 0 - 65535
+ ///
+ public int LifxBrightness => (int) _color.GetBrightness() * 65535;
+
+ ///
+ /// The temperature of this Color. The temperature ranges from 2700-9000
+ ///
+ public float K {
+ get;
+ set;
+ }
+
+ ///
+ /// Retrieve the base System.Drawing.Color of this Color
+ ///
+ public Color Color => _color;
+
+ ///
+ /// Create a new LifxColor
+ ///
+ public LifxColor() {
+ _color = Color.FromArgb(255, 0, 0, 0);
+ K = 2700;
+ }
+
+ ///
+ /// Create a color from HSBK values
+ ///
+ /// Hue: range 0 to 65535.
+ /// Saturation: range 0 to 65535.
+ /// Brightness: range 0 to 65535.
+ /// Kelvin: range 2500° (warm) to 9000° (cool). Default is 2700.
+ public LifxColor(ushort h, ushort s, ushort b, ushort k = 2700) {
+ var hue = h / 65535 * 360;
+ var sat = s / 65535f;
+ var bri = b / 65535f;
+ _color = HsbToRgb(hue, sat, bri);
+ K = k;
+ }
+
+ ///
+ /// Create a color from RGB Value, with default alpha of 255
+ ///
+ /// Red: Range 0 to 255
+ /// Green: Range 0 to 255
+ /// Blue: Range 0 to 255
+ public LifxColor(int r, int g, int b) {
+ _color = Color.FromArgb(255, r, g, b);
+ K = 2700;
+ }
+
+ ///
+ /// Create a LifxColor from a System.Drawing.Color
+ ///
+ /// Base System.Drawing.Color
+ public LifxColor(Color color) {
+ _color = color;
+ K = 2700;
+ }
+
+ ///
+ /// Serialize our color to a byte array
+ ///
+ /// HSBK formatted array of bytes.
+ public byte[] ToBytes() {
+ var output = new List();
+ var hue = Hue / 360 * 65535;
+ var sat = Saturation * 65535;
+ var bri = Brightness * 65535;
+ foreach (var u in new[] {(ushort) hue, (ushort) sat, (ushort) bri, (ushort) K}) {
+ output.AddRange(BitConverter.GetBytes(u));
+ }
+
+ return output.ToArray();
+ }
+
+
+ ///
+ /// Return System.Drawing.Color RGB string representation of the color
+ ///
+ ///
+ public string ToRgbString() {
+ return R + ", " + G + ", " + B;
+ }
+
+ ///
+ /// Return Lifx HSBK string representation of the color
+ ///
+ ///
+ public string ToHsbkString() {
+ var hue = Hue / 360 * 65535;
+ var sat = 65535 * Saturation;
+ var bri = 65536 * Brightness;
+ return hue + ", " + sat + ", " + bri + ", " + K;
+ }
+
+
+ ///
+ /// Converts HSB to RGB, with a specified output Alpha.
+ /// Arguments are limited to the defined range:
+ /// does not raise exceptions.
+ ///
+ /// Hue, must be in [0, 360].
+ /// Saturation, must be in [0, 1].
+ /// Brightness, must be in [0, 1].
+ /// Output Alpha, must be in [0, 255].
+ private static Color HsbToRgb(double h, double s, double b, int a = 255) {
+ h = Math.Max(0D, Math.Min(360D, h));
+ s = Math.Max(0D, Math.Min(1D, s));
+ b = Math.Max(0D, Math.Min(1D, b));
+ a = Math.Max(0, Math.Min(255, a));
+
+ var r = 0D;
+ var g = 0D;
+ var bl = 0D;
+
+ if (Math.Abs(s) < 0.000000000000001) {
+ r = g = bl = b;
+ } else {
+ // the argb wheel consists of 6 sectors. Figure out which sector
+ // you're in.
+ var sectorPos = h / 60D;
+ var sectorNumber = (int)Math.Floor(sectorPos);
+ // get the fractional part of the sector
+ var fractionalSector = sectorPos - sectorNumber;
+
+ // calculate values for the three axes of the argb.
+ var p = b * (1D - s);
+ var q = b * (1D - s * fractionalSector);
+ var t = b * (1D - s * (1D - fractionalSector));
+
+ // assign the fractional colors to r, g, and b based on the sector
+ // the angle is in.
+ switch (sectorNumber) {
+ case 0 :
+ r = b;
+ g = t;
+ bl = p;
+ break;
+ case 1 :
+ r = q;
+ g = b;
+ bl = p;
+ break;
+ case 2 :
+ r = p;
+ g = b;
+ bl = t;
+ break;
+ case 3 :
+ r = p;
+ g = q;
+ bl = b;
+ break;
+ case 4 :
+ r = t;
+ g = p;
+ bl = b;
+ break;
+ case 5 :
+ r = b;
+ g = p;
+ bl = q;
+ break;
+ }
+ }
+
+ return Color.FromArgb(
+ a,
+ Math.Max(0, Math.Min(255, Convert.ToInt32(double.Parse($"{r * 255D:0.00}")))),
+ Math.Max(0, Math.Min(255, Convert.ToInt32(double.Parse($"{g * 255D:0.00}")))),
+ Math.Max(0, Math.Min(255, Convert.ToInt32(double.Parse($"{bl * 250D:0.00}")))));
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/LifxNet/LifxNet.csproj b/src/LifxNet/LifxNet.csproj
index 1da0140..6745107 100644
--- a/src/LifxNet/LifxNet.csproj
+++ b/src/LifxNet/LifxNet.csproj
@@ -1,28 +1,31 @@
- netstandard1.3
+ netstandard2.0
LifxNet
LifxNet
true
Morten Nielsen
2.2.0
- Morten Nielsen
+ Digitalhigh, Morten Nielsen
Library for controlling LIFX light bulbs
- © Morten Nielsen 2015-2020
+ © Morten Nielsen 2015-2021
iot, lifx home automation uwp winphone winrt
MIT
- https://github.com/dotMorten/LifxNet
- https://github.com/dotMorten/LifxNet
+ https://github.com/d8ahazard/LifxNet
+ https://github.com/d8ahazard/LifxNet
true
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
git
8.0
enable
+ 2.2.5
+ Add initial support for Multizone
+
\ No newline at end of file
diff --git a/src/LifxNet/LifxPacket.cs b/src/LifxNet/LifxPacket.cs
index 176fe9d..1d5f302 100644
--- a/src/LifxNet/LifxPacket.cs
+++ b/src/LifxNet/LifxPacket.cs
@@ -1,53 +1,51 @@
using System;
-using System.Collections.Generic;
using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-namespace LifxNet
-{
- internal abstract class LifxPacket
- {
+namespace LifxNet {
+ internal abstract class LifxPacket {
private byte[] _payload;
private ushort _type;
- protected LifxPacket(ushort type, byte[] payload)
- {
+
+ protected LifxPacket(ushort type, byte[] payload) {
_type = type;
_payload = payload;
}
- internal byte[] Payload { get { return _payload; } }
- internal ushort Type { get { return _type; } }
- protected LifxPacket(ushort type, object[] data)
- {
+ internal byte[] Payload {
+ get { return _payload; }
+ }
+
+ internal ushort Type {
+ get { return _type; }
+ }
+
+ protected LifxPacket(ushort type, object[] data) {
_type = type;
- using (var ms = new MemoryStream())
- {
- StreamWriter bw = new StreamWriter(ms);
- foreach (var obj in data)
- {
- if (obj is byte)
- {
- bw.Write((byte)obj);
- }
- else if (obj is byte[])
- {
- bw.Write((byte[])obj);
- }
- else if (obj is UInt16)
- bw.Write((UInt16)obj);
- else if (obj is UInt32)
- bw.Write((UInt32)obj);
- else
+ using var ms = new MemoryStream();
+ StreamWriter bw = new StreamWriter(ms);
+ foreach (var obj in data) {
+ switch (obj) {
+ case byte b:
+ bw.Write(b);
+ break;
+ case byte[] bytes:
+ bw.Write(bytes);
+ break;
+ case ushort @ushort:
+ bw.Write(@ushort);
+ break;
+ case uint u:
+ bw.Write(u);
+ break;
+ default:
throw new NotImplementedException();
}
- _payload = ms.ToArray();
}
+
+ _payload = ms.ToArray();
}
- public static LifxPacket FromByteArray(byte[] data)
- {
+ public static LifxPacket FromByteArray(byte[] data) {
// preambleFields = [
// { name: "size" , type:type.uint16_le },
// { name: "protocol" , type:type.uint16_le },
@@ -73,26 +71,25 @@ public static LifxPacket FromByteArray(byte[] data)
ulong timestamp = br.ReadUInt64();
ushort packetType = br.ReadUInt16(); // ReverseBytes(br.ReadUInt16());
byte[] reserved4 = br.ReadBytes(2);
- byte[] payload = new byte[] { };
- if (len > 0)
- {
+ byte[] payload = { };
+ if (len > 0) {
payload = br.ReadBytes(len);
}
- LifxPacket packet = new UnknownPacket(packetType, payload, bulbAddress, site)
- {
+
+ LifxPacket packet = new UnknownPacket(packetType, payload, bulbAddress, site) {
TimeStamp = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(timestamp),
};
//packet.Identifier = identifier;
return packet;
}
- private class UnknownPacket : LifxPacket
- {
- public UnknownPacket(ushort packetType, byte[] payload, byte[] bulbAddress, byte[] site) : base(packetType, payload)
- {
+ private class UnknownPacket : LifxPacket {
+ public UnknownPacket(ushort packetType, byte[] payload, byte[] bulbAddress, byte[] site) : base(packetType,
+ payload) {
BulbAddress = bulbAddress;
Site = site;
}
+
public byte[] BulbAddress { get; }
public DateTime TimeStamp { get; set; }
public byte[] Site { get; set; }
diff --git a/src/LifxNet/LifxResponses.cs b/src/LifxNet/LifxResponses.cs
index 5357f78..0eb7258 100644
--- a/src/LifxNet/LifxResponses.cs
+++ b/src/LifxNet/LifxResponses.cs
@@ -1,43 +1,42 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.Diagnostics;
-namespace LifxNet
-{
+namespace LifxNet {
///
/// Base class for LIFX response types
///
- public abstract class LifxResponse
- {
- internal static LifxResponse Create(FrameHeader header, MessageType type, UInt32 source, byte[] payload)
- {
- switch (type)
- {
- case MessageType.DeviceAcknowledgement:
- return new AcknowledgementResponse(header, type, payload, source);
- case MessageType.DeviceStateLabel:
- return new StateLabelResponse(header, type, payload, source);
- case MessageType.LightState:
- return new LightStateResponse(header, type, payload, source);
- case MessageType.LightStatePower:
- return new LightPowerResponse(header, type, payload, source);
- case MessageType.InfraredState:
- return new InfraredStateRespone(header, type, payload, source);
- case MessageType.DeviceStateVersion:
- return new StateVersionResponse(header, type, payload, source);
- case MessageType.DeviceStateHostFirmware:
- return new StateHostFirmwareResponse(header, type, payload, source);
- case MessageType.DeviceStateService:
- return new StateServiceResponse(header, type, payload, source);
- default:
- return new UnknownResponse(header, type, payload, source);
- }
+ public abstract class LifxResponse {
+ internal static LifxResponse Create(FrameHeader header, MessageType type, uint source, Payload payload) {
+ return type switch {
+ MessageType.DeviceAcknowledgement => new AcknowledgementResponse(header, type, payload, source),
+ MessageType.DeviceStateLabel => new StateLabelResponse(header, type, payload, source),
+ MessageType.LightState => new LightStateResponse(header, type, payload, source),
+ MessageType.LightStatePower => new LightPowerResponse(header, type, payload, source),
+ MessageType.InfraredState => new InfraredStateResponse(header, type, payload, source),
+ MessageType.DeviceStateVersion => new StateVersionResponse(header, type, payload, source),
+ MessageType.DeviceStateHostFirmware => new StateHostFirmwareResponse(header, type, payload, source),
+ MessageType.DeviceStateService => new StateServiceResponse(header, type, payload, source),
+ MessageType.StateExtendedColorZones => new StateExtendedColorZonesResponse(header, type, payload,
+ source),
+ MessageType.StateZone => new StateZoneResponse(header, type, payload, source),
+ MessageType.StateMultiZone => new StateMultiZoneResponse(header, type, payload, source),
+ MessageType.StateDeviceChain => new StateDeviceChainResponse(header, type, payload, source),
+ MessageType.StateTileState64 => new StateTileState64Response(header, type, payload, source),
+ MessageType.StateRelayPower => new StateRelayPowerResponse(header, type, payload, source),
+ MessageType.DeviceStateHostInfo => new StateHostInfoResponse(header, type, payload, source),
+ MessageType.DeviceStateWifiInfo => new StateWifiInfoResponse(header, type, payload, source),
+ MessageType.DeviceStateWifiFirmware => new StateWifiFirmwareResponse(header, type, payload, source),
+ MessageType.DeviceStatePower => new StatePowerResponse(header, type, payload, source),
+ MessageType.DeviceStateInfo => new StateInfoResponse(header, type, payload, source),
+ MessageType.DeviceStateLocation => new StateLocationResponse(header, type, payload, source),
+ MessageType.DeviceStateGroup => new StateGroupResponse(header, type, payload, source),
+ MessageType.DeviceEchoResponse => new EchoResponse(header, type, payload, source),
+ _ => new UnknownResponse(header, type, payload, source)
+ };
}
- internal LifxResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source)
- {
+ internal LifxResponse(FrameHeader header, MessageType type, Payload payload, uint source) {
Header = header;
Type = type;
Payload = payload;
@@ -45,149 +44,523 @@ internal LifxResponse(FrameHeader header, MessageType type, byte[] payload, UInt
}
internal FrameHeader Header { get; }
- internal byte[] Payload { get; }
+ internal Payload Payload { get; }
internal MessageType Type { get; }
- internal UInt32 Source { get; }
+ internal uint Source { get; }
}
+
///
/// Response to any message sent with ack_required set to 1.
///
- internal class AcknowledgementResponse: LifxResponse
- {
- internal AcknowledgementResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) { }
+ internal class AcknowledgementResponse : LifxResponse {
+ internal AcknowledgementResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(
+ header, type, payload, source) {
+ }
+ }
+
+ ///
+ /// The StateZone message represents the state of a single zone with the index field indicating which zone is represented. The count field contains the count of the total number of zones available on the device.
+ ///
+ public class StateZoneResponse : LifxResponse {
+ internal StateZoneResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ Count = payload.GetUInt16();
+ Index = payload.GetUInt16();
+ Color = new Payload().GetColor();
+ }
+
+ ///
+ /// Count - total number of zones on the device
+ ///
+ public ushort Count { get; }
+
+ ///
+ /// Index - Zone the message starts from
+ ///
+ public ushort Index { get; }
+
+ ///
+ /// The list of colors returned by the message
+ ///
+ public LifxColor Color { get; }
+ }
+
+
+ ///
+ /// Response to GetHostInfo message.
+ /// Provides host MCU information.
+ ///
+ public class StateHostInfoResponse : LifxResponse {
+ internal StateHostInfoResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ Signal = payload.GetFloat32();
+ Tx = payload.GetUInt32();
+ Rx = payload.GetUInt32();
+ }
+
+ ///
+ /// Bytes received since power on
+ ///
+ public uint Rx { get; set; }
+
+ ///
+ /// Bytes transmitted since power on
+ ///
+ public uint Tx { get; set; }
+
+ ///
+ /// Radio receive signal strength in milliWatts
+ ///
+ public float Signal { get; set; }
+ }
+
+
+ ///
+ /// Response to GetWifiInfo message.
+ /// Provides host Wifi information.
+ ///
+ public class StateWifiInfoResponse : LifxResponse {
+ internal StateWifiInfoResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ Signal = payload.GetFloat32();
+ Tx = payload.GetUInt32();
+ Rx = payload.GetUInt32();
+ }
+
+ ///
+ /// Bytes received since power on
+ ///
+ public uint Rx { get; set; }
+
+ ///
+ /// Bytes transmitted since power on
+ ///
+ public uint Tx { get; set; }
+
+ ///
+ /// Radio receive signal strength in milliWatts
+ ///
+ public float Signal { get; set; }
+ }
+
+ ///
+ /// Response to GetWifiFirmware message.
+ /// Provides Wifi subsystem information.
+ ///
+ public class StateWifiFirmwareResponse : LifxResponse {
+ internal StateWifiFirmwareResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ Build = payload.GetUInt64();
+ // Skip 64-bit reserved
+ payload.Advance(8);
+ VersionMinor = payload.GetUInt16();
+ VersionMajor = payload.GetUInt16();
+ }
+
+ ///
+ /// Firmware build time (epoch time)
+ ///
+ public ulong Build { get; set; }
+ ///
+ /// Minor firmware version number
+ ///
+ public ushort VersionMinor { get; set; }
+ ///
+ /// Major firmware version number
+ ///
+ public ushort VersionMajor { get; set; }
+
+
+ }
+
+
+ ///
+ /// Provides device power level.
+ ///
+ public class StatePowerResponse : LifxResponse {
+ internal StatePowerResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ Level = payload.GetUInt16();
+ }
+
+ ///
+ /// Zero implies standby and non-zero sets a corresponding power draw level. Currently only 0 and 65535 are supported.
+ ///
+ public ulong Level { get; set; }
+
+ }
+
+
+ ///
+ /// Provides run-time information of device.
+ ///
+ public class StateInfoResponse : LifxResponse {
+ internal StateInfoResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ Time = DateTimeOffset.FromUnixTimeSeconds(payload.GetInt64()).DateTime;
+ Uptime = payload.GetInt64();
+ Downtime = payload.GetInt64();
+ }
+
+ ///
+ /// Current time
+ ///
+ public DateTime Time { get; set; }
+
+ ///
+ /// Time since last power on (relative time in nanoseconds)
+ ///
+ public long Uptime { get; set; }
+
+ ///
+ /// Last power off period, 5 second accuracy (in nanoseconds)
+ ///
+ public long Downtime { get; set; }
+ }
+
+
+ ///
+ /// Device location.
+ ///
+ public class StateLocationResponse : LifxResponse {
+ internal StateLocationResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ Location = payload.GetBytes(16);
+ Label = payload.GetString(32);
+ Updated = payload.GetUInt64();
+ }
+
+ public byte[] Location { get; set; }
+
+ public string Label { get; set; }
+
+ public ulong Updated { get; set; }
+ }
+
+ ///
+ /// Device group.
+ ///
+ public class StateGroupResponse : LifxResponse {
+ internal StateGroupResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ Group = payload.GetBytes(16);
+ Label = payload.GetString(32);
+ Updated = payload.GetUInt64();
+ }
+
+ public byte[] Group { get; set; }
+
+ public string Label { get; set; }
+
+ public ulong Updated { get; set; }
+ }
+
+ ///
+ /// Echo response with payload sent in the EchoRequest.
+ ///
+ public class EchoResponse : LifxResponse {
+ internal EchoResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ RequestPayload = payload.ToArray();
+ }
+
+ ///
+ /// Payload sent in the EchoRequest.
+ ///
+ public byte[] RequestPayload { get; set; }
+ }
+
+
+ ///
+ /// The StateZone message represents the state of a single zone with the index field indicating which zone is represented. The count field contains the count of the total number of zones available on the device.
+ ///
+ public class StateDeviceChainResponse : LifxResponse {
+ internal StateDeviceChainResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(
+ header,
+ type, payload, source) {
+ Tiles = new List();
+ StartIndex = payload.GetUint8();
+ for (var i = 0; i < 16; i++) {
+ var tile = new Tile();
+ tile.LoadBytes(payload);
+ Tiles.Add(tile);
+ }
+ TotalCount = payload.GetUint8();
+ }
+
+ ///
+ /// Count - total number of zones on the device
+ ///
+ public int TotalCount { get; }
+
+ ///
+ /// Start Index - Zone the message starts from
+ ///
+ public byte StartIndex { get; }
+
+ ///
+ /// The list of colors returned by the message
+ ///
+ public List Tiles { get; }
+ }
+
+ ///
+ /// Get the list of colors currently being displayed by zones
+ ///
+ public class StateMultiZoneResponse : LifxResponse {
+ internal StateMultiZoneResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(
+ header, type, payload, source) {
+ Colors = new LifxColor[8];
+ Count = payload.GetUint8();
+ Index = payload.GetUint8();
+ for (var i = 0; i < 8; i++) {
+ Debug.WriteLine($"Reading color {i}.");
+ Colors[i] = payload.GetColor();
+ }
+ Debug.WriteLine("Colors read.");
+ }
+
+ ///
+ /// Count - total number of zones on the device
+ ///
+ public ushort Count { get; }
+
+ ///
+ /// Index - Zone the message starts from
+ ///
+ public ushort Index { get; }
+
+ ///
+ /// The list of colors returned by the message
+ ///
+ public LifxColor[] Colors { get; }
+ }
+
+
+ ///
+ /// Get the list of colors currently being displayed by zones
+ ///
+ public class StateExtendedColorZonesResponse : LifxResponse {
+ internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, Payload payload, uint source) :
+ base(header, type, payload, source) {
+ Colors = new List();
+ Count = payload.GetUInt16();
+ Index = payload.GetUInt16();
+ while (payload.HasContent()) {
+ Colors.Add(payload.GetColor());
+ }
+ }
+
+ ///
+ /// Count - total number of zones on the device
+ ///
+ public ushort Count { get; private set; }
+
+ ///
+ /// Index - Zone the message starts from
+ ///
+ public ushort Index { get; private set; }
+
+ ///
+ /// The list of colors returned by the message
+ ///
+ public List Colors { get; private set; }
}
+
///
/// Response to GetService message.
/// Provides the device Service and port.
/// If the Service is temporarily unavailable, then the port value will be 0.
///
- internal class StateServiceResponse : LifxResponse
- {
- internal StateServiceResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source)
- {
- Service = payload[0];
- Port = BitConverter.ToUInt32(payload, 1);
+ internal class StateServiceResponse : LifxResponse {
+ internal StateServiceResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(
+ header, type, payload, source) {
+ Service = payload.GetUint8();
+ Port = payload.GetUInt32();
}
- public Byte Service { get; }
- public UInt32 Port { get; }
+
+ private byte Service { get; }
+ private uint Port { get; }
}
+
+ ///
+ /// Response to any message sent with ack_required set to 1.
+ ///
+ public class StateTileState64Response : LifxResponse {
+ internal StateTileState64Response(FrameHeader header, MessageType type, Payload payload, uint source) : base(
+ header, type, payload, source) {
+ TileIndex = payload.GetUint8();
+ // Skip one byte for reserved
+ payload.Advance();
+ X = payload.GetUint8();
+ Y = payload.GetUint8();
+ Width = payload.GetUint8();
+ Colors = new LifxColor[64];
+ for (var i = 0; i < Colors.Length; i++) {
+ if (payload.HasContent()) {
+ Colors[i] = payload.GetColor();
+ } else {
+ Debug.WriteLine($"Content size mismatch fetching colors: {i}/64: ");
+ }
+ }
+ }
+
+ public uint TileIndex { get; }
+ public uint X { get; }
+ public uint Y { get; }
+ public uint Width { get; }
+ public LifxColor[] Colors { get; }
+ }
+
///
/// Response to GetLabel message. Provides device label.
///
- internal class StateLabelResponse : LifxResponse
- {
- internal StateLabelResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source)
- {
- if (payload != null)
- Label = Encoding.UTF8.GetString(payload, 0, payload.Length).Replace("\0", "");
+ internal class StateLabelResponse : LifxResponse {
+ internal StateLabelResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ Label = payload.GetString().Replace("\0", "");
}
- public string? Label { get; private set; }
+
+ public string? Label { get; }
}
+
///
/// Sent by a device to provide the current light state
///
- public class LightStateResponse : LifxResponse
- {
- internal LightStateResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source)
- {
- Hue = BitConverter.ToUInt16(payload, 0);
- Saturation = BitConverter.ToUInt16(payload, 2);
- Brightness = BitConverter.ToUInt16(payload, 4);
- Kelvin = BitConverter.ToUInt16(payload, 6);
- IsOn = BitConverter.ToUInt16(payload, 10) > 0;
- Label = Encoding.UTF8.GetString(payload, 12, 32).Replace("\0","");
+ public class LightStateResponse : LifxResponse {
+ internal LightStateResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ Hue = payload.GetUInt16();
+ Saturation = payload.GetUInt16();
+ Brightness = payload.GetUInt16();
+ Kelvin = payload.GetUInt16();
+ IsOn = payload.GetUInt16() > 0;
+ Label = payload.GetString(32).Replace("\\0", "");
}
+
///
/// Hue
///
- public UInt16 Hue { get; private set; }
+ public ushort Hue { get; }
+
///
/// Saturation (0=desaturated, 65535 = fully saturated)
///
- public UInt16 Saturation { get; private set; }
+ public ushort Saturation { get; }
+
///
/// Brightness (0=off, 65535=full brightness)
///
- public UInt16 Brightness { get; private set; }
+ public ushort Brightness { get; }
+
///
/// Bulb color temperature
///
- public UInt16 Kelvin { get; private set; }
+ public ushort Kelvin { get; }
+
///
/// Power state
///
- public bool IsOn { get; private set; }
+ public bool IsOn { get; }
+
///
/// Light label
///
- public string Label { get; private set; }
+ public string Label { get; }
}
- internal class LightPowerResponse : LifxResponse
- {
- internal LightPowerResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source)
- {
- IsOn = BitConverter.ToUInt16(payload, 0) > 0;
+
+ internal class LightPowerResponse : LifxResponse {
+ internal LightPowerResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ IsOn = payload.GetUInt16() > 0;
}
- public bool IsOn { get; private set; }
+
+ public bool IsOn { get; }
}
- internal class InfraredStateRespone : LifxResponse
- {
- internal InfraredStateRespone(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source)
- {
- Brightness = BitConverter.ToUInt16(payload, 0);
+ internal class InfraredStateResponse : LifxResponse {
+ internal InfraredStateResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(
+ header, type, payload, source) {
+ Brightness = payload.GetUInt16();
}
- public UInt16 Brightness { get; private set; }
+
+ public ushort Brightness { get; }
}
///
/// Response to GetVersion message. Provides the hardware version of the device.
///
- public class StateVersionResponse : LifxResponse
- {
- internal StateVersionResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source)
- {
- Vendor = BitConverter.ToUInt32(payload, 0);
- Product = BitConverter.ToUInt32(payload, 4);
- Version = BitConverter.ToUInt32(payload, 8);
+ public class StateVersionResponse : LifxResponse {
+ internal StateVersionResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(
+ header, type, payload, source) {
+ Vendor = Payload.GetUInt32();
+ Product = Payload.GetUInt32();
+ Version = Payload.GetUInt32();
}
+
///
/// Vendor ID
///
- public UInt32 Vendor { get; private set; }
+ public uint Vendor { get; }
+
///
/// Product ID
///
- public UInt32 Product { get; private set; }
+ public uint Product { get; }
+
///
/// Hardware version
///
- public UInt32 Version { get; private set; }
+ public uint Version { get; }
}
+
///
/// Response to GetHostFirmware message. Provides host firmware information.
///
- public class StateHostFirmwareResponse : LifxResponse
- {
- internal StateHostFirmwareResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source)
- {
- var nanoseconds = BitConverter.ToUInt64(payload, 0);
+ public class StateHostFirmwareResponse : LifxResponse {
+ internal StateHostFirmwareResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(
+ header, type, payload, source) {
+ var nanoseconds = payload.GetUInt64();
Build = Utilities.Epoch.AddMilliseconds(nanoseconds * 0.000001);
//8..15 UInt64 is reserved
- Version = BitConverter.ToUInt32(payload, 16);
+ Version = payload.GetUInt32();
}
+
///
/// Firmware build time
///
- public DateTime Build { get; private set; }
+ public DateTime Build { get; }
+
///
/// Firmware version
///
- public UInt32 Version { get; private set; }
+ public uint Version { get; }
}
- internal class UnknownResponse : LifxResponse
- {
- internal UnknownResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) { }
+ ///
+ /// Response to GetVersion message. Provides the hardware version of the device.
+ ///
+ public class StateRelayPowerResponse : LifxResponse {
+ internal StateRelayPowerResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(
+ header, type, payload, source) {
+ RelayIndex = payload.GetUint8();
+ Level = payload.GetUInt16();
+ }
+
+ ///
+ /// The relay on the switch starting from 0
+ ///
+ public int RelayIndex { get; }
+
+ ///
+ /// The value of the relay
+ ///
+ public int Level { get; }
+ }
+
+ internal class UnknownResponse : LifxResponse {
+ internal UnknownResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header,
+ type, payload, source) {
+ }
}
-}
+}
\ No newline at end of file
diff --git a/src/LifxNet/LightBulb.cs b/src/LifxNet/LightBulb.cs
new file mode 100644
index 0000000..a1e2e09
--- /dev/null
+++ b/src/LifxNet/LightBulb.cs
@@ -0,0 +1,19 @@
+namespace LifxNet {
+ ///
+ /// LIFX light bulb
+ ///
+ public sealed class LightBulb : Device {
+ ///
+ /// Initializes a new instance of a bulb instead of relying on discovery. At least the host name must be provide for the device to be usable.
+ ///
+ /// Required
+ ///
+ ///
+ ///
+ ///
+ public LightBulb(string hostname, byte[] macAddress, byte service = 0, uint port = 0, uint productId = 0) :
+ base(hostname,
+ macAddress, service, port) {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LifxNet/MessageType.cs b/src/LifxNet/MessageType.cs
index fb34d9b..eab1a66 100644
--- a/src/LifxNet/MessageType.cs
+++ b/src/LifxNet/MessageType.cs
@@ -1,19 +1,13 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace LifxNet
-{
- internal enum MessageType : ushort
- {
+namespace LifxNet {
+ internal enum MessageType : ushort {
//Device Messages
DeviceGetService = 0x02,
DeviceStateService = 0x03,
+ //Undocumented?
DeviceGetTime = 0x04,
DeviceSetTime = 0x05,
DeviceStateTime = 0x06,
+ // Documented
DeviceGetHostInfo = 12,
DeviceStateHostInfo = 13,
DeviceGetHostFirmware = 14,
@@ -41,6 +35,7 @@ internal enum MessageType : ushort
DeviceStateGroup = 53,
DeviceEchoRequest = 58,
DeviceEchoResponse = 59,
+
//Light messages
LightGet = 101,
LightSetColor = 102,
@@ -50,14 +45,38 @@ internal enum MessageType : ushort
LightSetPower = 117,
LightStatePower = 118,
LightSetWaveformOptional = 119,
+
//Infrared
InfraredGet = 120,
InfraredState = 121,
InfraredSet = 122,
+ //Multi zone
+ SetColorZones = 501,
+ GetColorZones = 502,
+ StateZone = 503,
+ StateMultiZone = 506,
+ SetExtendedColorZones = 510,
+ GetExtendedColorZones = 511,
+ StateExtendedColorZones = 512,
+
+ //Tile
+ GetDeviceChain = 701,
+ StateDeviceChain = 702,
+ SetUserPosition = 703,
+ GetTileState64 = 707,
+ StateTileState64 = 711,
+ SetTileState64 = 715,
+
+ //Switch
+ GetRelayPower = 816,
+ SetRelayPower = 817,
+ StateRelayPower = 818,
+
//Unofficial
LightGetTemperature = 0x6E,
- //LightStateTemperature = 0x6f,
+
+ //LightStateTemperature = 0x6f,
SetLightBrightness = 0x68
}
-}
+}
\ No newline at end of file
diff --git a/src/LifxNet/Payload.cs b/src/LifxNet/Payload.cs
new file mode 100644
index 0000000..face24c
--- /dev/null
+++ b/src/LifxNet/Payload.cs
@@ -0,0 +1,422 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace LifxNet {
+ ///
+ /// A wrapper class for a byte payload
+ /// Any time the payload is read from, our pointer increments
+ /// the proper number of bytes until the end is reached,
+ /// at which time a message will be logged. Should eventually throw an error or something...
+ ///
+ public class Payload {
+ private readonly List _data;
+ private readonly BinaryReader _br;
+ private readonly MemoryStream _ms;
+ private readonly long _len;
+
+ ///
+ /// Get the length of the internal byte array
+ ///
+ public int Length => _data.Count;
+
+
+ ///
+ /// Initialize a new, empty payload where we can serialize outgoing data
+ ///
+ public Payload() {
+ _data = new List();
+ _ms = new MemoryStream(_data.ToArray());
+ _len = _ms.Length;
+ _br = new BinaryReader(_ms);
+ }
+
+ ///
+ /// Create a payload from an array of objects
+ ///
+ ///
+ ///
+ public Payload(Object[] args) {
+ _data = new List();
+ foreach (var arg in args) {
+ switch (arg) {
+ case byte b:
+ Add(b);
+ break;
+ case ushort @ushort:
+ Add((byte)@ushort);
+ break;
+ case uint u:
+ Add(u);
+ break;
+ case byte[] bytes:
+ Add(bytes);
+ break;
+ case string s:
+ Add(s.PadRight(32).Take(32).ToString());
+ break;
+ case long l:
+ Add(l);
+ break;
+ case double d:
+ Add(d);
+ break;
+ case float f:
+ Add(f);
+ break;
+ case short s:
+ Add(s);
+ break;
+ case int i:
+ Add(i);
+ break;
+ case ulong u:
+ Add(u);
+ break;
+ case LifxColor c:
+ Add(c);
+ break;
+ case LifxColor[] colors:
+ Add(colors);
+ break;
+ case DateTime dt:
+ Add(dt);
+ break;
+ case Tile t:
+ Add(t);
+ break;
+ default:
+ Debug.WriteLine("Unsupported type!" + args.GetType().FullName);
+ throw new NotSupportedException(args.GetType().FullName);
+ }
+ }
+
+ _ms = new MemoryStream(_data.ToArray());
+ _len = _ms.Length;
+ _br = new BinaryReader(_ms);
+ }
+
+ ///
+ /// Initialize with a byte array
+ ///
+ ///
+ public Payload(byte[] data) {
+ _data = data.ToList();
+ _ms = new MemoryStream(data);
+ _len = _ms.Length;
+ _br = new BinaryReader(_ms);
+ }
+
+ ///
+ /// Get the current position of the array
+ ///
+ public int Position => (int) _ms.Position;
+
+ ///
+ /// Return our base byte list as an array
+ ///
+ ///
+ public byte[] ToArray() {
+ return _data.ToArray();
+ }
+
+ ///
+ /// Return our base byte list
+ ///
+ ///
+ public List ToList() {
+ return _data;
+ }
+
+ ///
+ /// Serialize base byte list to a string
+ ///
+ ///
+ public override string ToString() {
+ return _data.ToString();
+ }
+
+ ///
+ /// Check to see if we still have data to read
+ ///
+ ///
+ public bool HasContent() {
+ return _ms.Position < _len;
+ }
+
+ ///
+ /// Rewind our pointer N bytes
+ ///
+ /// How far to rewind. Default is 1.
+ public void Rewind(int len = 1) {
+ if (_ms.Position - len < 0) {
+ Reset();
+ } else {
+ _ms.Seek(len * -1, SeekOrigin.Current);
+ }
+ }
+
+ ///
+ /// Forward our pointer N bytes
+ ///
+ /// How far to advance. Default is 1.
+ public void Advance(int len = 1) {
+ if (_ms.Position + len < _len) {
+ _ms.Seek(len, SeekOrigin.Current);
+ } else {
+ FastForward();
+ }
+ }
+
+ ///
+ /// Forward the pointer to the end of the array
+ ///
+ public void FastForward() {
+ _ms.Seek(0, SeekOrigin.End);
+ }
+
+ ///
+ /// Reset our pointer to 0
+ ///
+ public void Reset() {
+ _ms.Seek(0, SeekOrigin.Begin);
+ }
+
+ ///
+ /// Read LifxColor from array and increment pointer 8 bytes
+ ///
+ ///
+ public LifxColor GetColor() {
+ try {
+ var h = GetUInt16();
+ var s = GetUInt16();
+ var b = GetUInt16();
+ var k = GetUInt16();
+ return new LifxColor(h, s, b, k);
+ } catch (Exception e) {
+ Debug.WriteLine("Exception: " + e.Message);
+ }
+
+ return new LifxColor();
+ }
+
+ ///
+ /// Get an array of bytes from the reader
+ ///
+ ///
+ ///
+ public byte[] GetBytes(int len) {
+ return _br.ReadBytes(len);
+ }
+
+ ///
+ /// Read Uint8 from array and increment pointer 1 byte
+ ///
+ /// byte
+ public byte GetUint8() {
+ try {
+ return _br.ReadByte();
+ } catch {
+ Debug.WriteLine("Error reading byte, pos is " + _ms.Position);
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Read UInt16 from array and increment pointer 2 bytes
+ ///
+ /// ushort
+ public ushort GetUInt16() {
+ try {
+ return _br.ReadUInt16();
+ } catch {
+ Debug.WriteLine($"Error getting Uint16 from payload, pointer {_ms.Position} of range: " + _len);
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Read Int16 from array and increment pointer 2 bytes.
+ ///
+ /// short
+ public short GetInt16() {
+ try {
+ return _br.ReadInt16();
+ } catch {
+ Debug.WriteLine($"Error getting int16 from payload, pointer {_ms.Position} of range: " + _len);
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Read Int32 from array and increment pointer 4 bytes.
+ ///
+ /// int
+ public int GetInt32() {
+ try {
+ return _br.ReadInt32();
+ } catch {
+ Debug.WriteLine($"Error getting Int32 from payload, pointer {_ms.Position} of range: " + _len);
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Read a UInt32 from array and increment pointer 4 bytes.
+ ///
+ ///
+ public uint GetUInt32() {
+ try {
+ return _br.ReadUInt32();
+ } catch {
+ Debug.WriteLine($"Error getting Uint32 from payload, pointer {_ms.Position} of range: " + _len);
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Read an Int64 from array and increment pointer 8 bytes.
+ ///
+ /// long
+ public long GetInt64() {
+ try {
+ return _br.ReadInt64();
+ } catch {
+ Debug.WriteLine($"Error getting Int64 from payload, pointer {_ms.Position} of range: " + _len);
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Read a UInt64 from array and increment pointer 8 bytes.
+ ///
+ /// ulong
+ public ulong GetUInt64() {
+ try {
+ return _br.ReadUInt64();
+ } catch {
+ Debug.WriteLine($"Error getting Uint64 from payload, pointer {_ms.Position} of range: " + _len);
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Read a Float32 from array and increment pointer 4 bytes.
+ ///
+ /// float
+ public float GetFloat32() {
+ try {
+ return _br.ReadSingle();
+ } catch {
+ Debug.WriteLine($"Error getting Float32 from payload, pointer {_ms.Position} of range: " + _len);
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Read a string from our payload.
+ ///
+ /// The number of chars to read. If none specified, will read the entire payload
+ /// string
+ public string GetString(long length = -1) {
+ if (length == -1) length = _len - 1 - _ms.Position;
+ try {
+ return _br.ReadChars((int) length).ToString();
+ } catch {
+ Debug.WriteLine($"Error getting string, pointer {_ms.Position} out of range: " + _len);
+ }
+
+ return string.Empty;
+ }
+
+ private void Add(string input) {
+ _data.AddRange(Encoding.ASCII.GetBytes(input));
+ }
+
+ private void Add(byte input) {
+ _data.Add(input);
+ }
+
+ private void Add(byte[] input) {
+ _data.AddRange(input);
+ }
+
+ private void Add(short input) {
+ _data.AddRange(BitConverter.GetBytes(input));
+ }
+
+ private void Add(int input) {
+ _data.AddRange(BitConverter.GetBytes(input));
+ }
+
+ private void Add(uint input) {
+ _data.AddRange(BitConverter.GetBytes(input));
+ }
+
+ private void Add(long input) {
+ _data.AddRange(BitConverter.GetBytes(input));
+ }
+
+ private void Add(float input) {
+ _data.AddRange(BitConverter.GetBytes(input));
+ }
+
+ private void Add(double input) {
+ _data.AddRange(BitConverter.GetBytes(input));
+ }
+
+ private void Add(ushort input) {
+ _data.AddRange(BitConverter.GetBytes(input));
+ }
+
+ private void Add(ulong input) {
+ _data.AddRange(BitConverter.GetBytes(input));
+ }
+
+ private void Add(LifxColor input) {
+ _data.AddRange(input.ToBytes());
+ }
+
+ private void Add(LifxColor[] input) {
+ foreach (var lc in input) {
+ Add(lc);
+ }
+ }
+
+ private void Add(DateTime input) {
+ var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ Add(Convert.ToInt64((input - epoch).TotalSeconds));
+ }
+
+ private void Add(Tile input) {
+ Add(input.AccelMeasX);
+ Add(input.AccelMeasY);
+ Add(input.AccelMeasZ);
+ Add(0);
+ Add(input.UserX);
+ Add(input.UserY);
+ Add(input.Width);
+ Add(input.Height);
+ Add((byte) 0);
+ Add(input.DeviceVersionVendor);
+ Add(input.DeviceVersionProduct);
+ Add(input.DeviceVersionVersion);
+ Add(input.FirmwareBuild);
+ Add((ulong) 0);
+ Add(input.FirmwareVersionMinor);
+ Add(input.FirmwareVersionMajor);
+ Add((uint) 0);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LifxNet/TileGroup.cs b/src/LifxNet/TileGroup.cs
new file mode 100644
index 0000000..955f6a2
--- /dev/null
+++ b/src/LifxNet/TileGroup.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Diagnostics;
+
+namespace LifxNet {
+ [Serializable]
+ public class Tile {
+ public short AccelMeasX { get; set; }
+ public short AccelMeasY { get; set; }
+ public short AccelMeasZ { get; set; }
+ public float UserX { get; set; }
+ public float UserY { get; set; }
+ public byte Width { get; set; }
+ public byte Height { get; set; }
+ public uint DeviceVersionVendor { get; set; }
+ public uint DeviceVersionProduct { get; set; }
+ public uint DeviceVersionVersion { get; set; }
+ public ulong FirmwareBuild { get; set; }
+ public ushort FirmwareVersionMinor { get; set; }
+ public ushort FirmwareVersionMajor { get; set; }
+
+ public Tile() {
+ }
+
+ public void CreateDefault(int index) {
+ AccelMeasX = 0;
+ AccelMeasY = 0;
+ AccelMeasZ = 0;
+ UserX = index * .5f;
+ UserY = 8.06f;
+ Width = 8;
+ Height = 8;
+ DeviceVersionProduct = 55;
+ DeviceVersionVendor = 1;
+ DeviceVersionVersion = 10;
+ FirmwareBuild = 1532997580;
+ FirmwareVersionMajor = 50;
+ FirmwareVersionMinor = 3;
+ }
+
+ ///
+ /// Read payload into tile
+ ///
+ ///
+ public void LoadBytes(Payload payload) {
+
+ AccelMeasX = payload.GetInt16();
+ AccelMeasY = payload.GetInt16();
+ AccelMeasZ = payload.GetInt16();
+ payload.Advance(2);
+ UserX = payload.GetFloat32();
+ UserY = payload.GetFloat32();
+ Width = payload.GetUint8();
+ Height = payload.GetUint8();
+ payload.Advance();
+ DeviceVersionVendor = payload.GetUInt32();
+ DeviceVersionProduct = payload.GetUInt32();
+ DeviceVersionVersion = payload.GetUInt32();
+ FirmwareBuild = payload.GetUInt64();
+ payload.Advance(8);
+ FirmwareVersionMajor = payload.GetUInt16();
+ FirmwareVersionMinor = payload.GetUInt16();
+ payload.Advance(4);
+ }
+
+ public byte[] ToBytes() {
+ var output = new List();
+ output.AddRange(BitConverter.GetBytes(AccelMeasX));
+ output.AddRange(BitConverter.GetBytes(AccelMeasY));
+ output.AddRange(BitConverter.GetBytes(AccelMeasZ));
+ output.AddRange(BitConverter.GetBytes((short) 0));
+ output.AddRange(BitConverter.GetBytes(UserX));
+ output.AddRange(BitConverter.GetBytes(UserY));
+ output.Add(Width);
+ output.Add(Height);
+ output.Add(50); // Reserved
+ output.AddRange(BitConverter.GetBytes(DeviceVersionVendor));
+ output.AddRange(BitConverter.GetBytes(DeviceVersionProduct));
+ output.AddRange(BitConverter.GetBytes(DeviceVersionVersion));
+ output.AddRange(BitConverter.GetBytes(FirmwareBuild));
+ output.AddRange(BitConverter.GetBytes(FirmwareBuild));
+ output.AddRange(BitConverter.GetBytes(FirmwareVersionMajor));
+ output.AddRange(BitConverter.GetBytes(FirmwareVersionMinor));
+ output.AddRange(BitConverter.GetBytes(uint.MinValue)); // Reserved
+
+ return output.ToArray();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/LifxNet/Utilities.cs b/src/LifxNet/Utilities.cs
index bf280e6..6879a6b 100644
--- a/src/LifxNet/Utilities.cs
+++ b/src/LifxNet/Utilities.cs
@@ -1,17 +1,10 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-namespace LifxNet
-{
- internal static class Utilities
- {
+namespace LifxNet {
+ internal static class Utilities {
public static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
-
- public static UInt16[] RgbToHsl(Color rgb)
- {
+
+ public static ushort[] RgbToHsl(LifxColor rgb) {
// normalize red, green and blue values
double r = (rgb.R / 255.0);
double g = (rgb.G / 255.0);
@@ -21,31 +14,22 @@ public static UInt16[] RgbToHsl(Color rgb)
double min = Math.Min(r, Math.Min(g, b));
double h = 0.0;
- if (max == r && g >= b)
- {
+ if (max == r && g >= b) {
h = 60 * (g - b) / (max - min);
- }
- else if (max == r && g < b)
- {
+ } else if (max == r && g < b) {
h = 60 * (g - b) / (max - min) + 360;
- }
- else if (max == g)
- {
+ } else if (max == g) {
h = 60 * (b - r) / (max - min) + 120;
- }
- else if (max == b)
- {
+ } else if (max == b) {
h = 60 * (r - g) / (max - min) + 240;
}
double s = (max == 0) ? 0.0 : (1.0 - (min / max));
- return new UInt16[] {
- (UInt16)(h / 360 * 65535),
- (UInt16)(s * 65535),
- (UInt16)(max * 65535)
+ return new[] {
+ (ushort) (h / 360 * 65535),
+ (ushort) (s * 65535),
+ (ushort) (max * 65535)
};
}
-
-
}
-}
+}
\ No newline at end of file
diff --git a/src/LifxNet/json/products.json b/src/LifxNet/json/products.json
new file mode 100644
index 0000000..14c6fa0
--- /dev/null
+++ b/src/LifxNet/json/products.json
@@ -0,0 +1,916 @@
+[
+ {
+ "vid": 1,
+ "name": "LIFX",
+ "products": [
+ {
+ "pid": 1,
+ "name": "LIFX Original 1000",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 3,
+ "name": "LIFX Color 650",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 10,
+ "name": "LIFX White 800 (Low Voltage)",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2700,
+ 6500
+ ]
+ }
+ },
+ {
+ "pid": 11,
+ "name": "LIFX White 800 (High Voltage)",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2700,
+ 6500
+ ]
+ }
+ },
+ {
+ "pid": 15,
+ "name": "LIFX Color 1000",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 18,
+ "name": "LIFX White 900 BR30 (Low Voltage)",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 19,
+ "name": "LIFX White 900 BR30 (High Voltage)",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 20,
+ "name": "LIFX Color 1000 BR30",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 22,
+ "name": "LIFX Color 1000",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 27,
+ "name": "LIFX A19",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 28,
+ "name": "LIFX BR30",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 29,
+ "name": "LIFX A19 Night Vision",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": true,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 30,
+ "name": "LIFX BR30 Night Vision",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": true,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 31,
+ "name": "LIFX Z",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": true,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 32,
+ "name": "LIFX Z",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": true,
+ "temperature_range": [
+ 2500,
+ 9000
+ ],
+ "min_ext_mz_firmware": 1532997580,
+ "min_ext_mz_firmware_components": [
+ 2,
+ 77
+ ]
+ }
+ },
+ {
+ "pid": 36,
+ "name": "LIFX Downlight",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 37,
+ "name": "LIFX Downlight",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 38,
+ "name": "LIFX Beam",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": true,
+ "temperature_range": [
+ 2500,
+ 9000
+ ],
+ "min_ext_mz_firmware": 1532997580,
+ "min_ext_mz_firmware_components": [
+ 2,
+ 77
+ ]
+ }
+ },
+ {
+ "pid": 39,
+ "name": "LIFX Downlight White To Warm",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 1500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 40,
+ "name": "LIFX Downlight",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 43,
+ "name": "LIFX A19",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 44,
+ "name": "LIFX BR30",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 45,
+ "name": "LIFX A19 Night Vision",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": true,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 46,
+ "name": "LIFX BR30 Night Vision",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": true,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 49,
+ "name": "LIFX Mini Color",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 50,
+ "name": "LIFX Mini White To Warm",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 1500,
+ 4000
+ ]
+ }
+ },
+ {
+ "pid": 51,
+ "name": "LIFX Mini White",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2700,
+ 2700
+ ]
+ }
+ },
+ {
+ "pid": 52,
+ "name": "LIFX GU10",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 53,
+ "name": "LIFX GU10",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 55,
+ "name": "LIFX Tile",
+ "features": {
+ "color": true,
+ "chain": true,
+ "matrix": true,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 57,
+ "name": "LIFX Candle",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": true,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 1500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 59,
+ "name": "LIFX Mini Color",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 60,
+ "name": "LIFX Mini White To Warm",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 1500,
+ 4000
+ ]
+ }
+ },
+ {
+ "pid": 61,
+ "name": "LIFX Mini White",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2700,
+ 2700
+ ]
+ }
+ },
+ {
+ "pid": 62,
+ "name": "LIFX A19",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 63,
+ "name": "LIFX BR30",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 64,
+ "name": "LIFX A19 Night Vision",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": true,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 65,
+ "name": "LIFX BR30 Night Vision",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": true,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 66,
+ "name": "LIFX Mini White",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2700,
+ 2700
+ ]
+ }
+ },
+ {
+ "pid": 68,
+ "name": "LIFX Candle",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": true,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 1500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 70,
+ "name": "LIFX Switch",
+ "features": {
+ "color": false,
+ "relays": true,
+ "chain": false,
+ "matrix": false,
+ "buttons": true,
+ "infrared": false,
+ "multizone": false
+ }
+ },
+ {
+ "pid": 81,
+ "name": "LIFX Candle White To Warm",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2200,
+ 6500
+ ]
+ }
+ },
+ {
+ "pid": 82,
+ "name": "LIFX Filament Clear",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2100,
+ 2100
+ ]
+ }
+ },
+ {
+ "pid": 85,
+ "name": "LIFX Filament Amber",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2000,
+ 2000
+ ]
+ }
+ },
+ {
+ "pid": 87,
+ "name": "LIFX Mini White",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2700,
+ 2700
+ ]
+ }
+ },
+ {
+ "pid": 88,
+ "name": "LIFX Mini White",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2700,
+ 2700
+ ]
+ }
+ },
+ {
+ "pid": 89,
+ "name": "LIFX Switch",
+ "features": {
+ "color": false,
+ "relays": true,
+ "chain": false,
+ "matrix": false,
+ "buttons": true,
+ "infrared": false,
+ "multizone": false
+ }
+ },
+ {
+ "pid": 90,
+ "name": "LIFX Clean",
+ "features": {
+ "hev": true,
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 91,
+ "name": "LIFX Color",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 92,
+ "name": "LIFX Color",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 94,
+ "name": "LIFX BR30",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 96,
+ "name": "LIFX Candle White To Warm",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2200,
+ 6500
+ ]
+ }
+ },
+ {
+ "pid": 97,
+ "name": "LIFX A19",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 98,
+ "name": "LIFX BR30",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 99,
+ "name": "LIFX Clean",
+ "features": {
+ "hev": true,
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 100,
+ "name": "LIFX Filament Clear",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2100,
+ 2100
+ ]
+ }
+ },
+ {
+ "pid": 101,
+ "name": "LIFX Filament Amber",
+ "features": {
+ "color": false,
+ "chain": false,
+ "matrix": false,
+ "infrared": false,
+ "multizone": false,
+ "temperature_range": [
+ 2000,
+ 2000
+ ]
+ }
+ },
+ {
+ "pid": 109,
+ "name": "LIFX A19 Night Vision",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": true,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 110,
+ "name": "LIFX BR30 Night Vision",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": true,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ },
+ {
+ "pid": 111,
+ "name": "LIFX A19 Night Vision",
+ "features": {
+ "color": true,
+ "chain": false,
+ "matrix": false,
+ "infrared": true,
+ "multizone": false,
+ "temperature_range": [
+ 2500,
+ 9000
+ ]
+ }
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/src/SampleApps/ColorSendTest/ColorSendTest.csproj b/src/SampleApps/ColorSendTest/ColorSendTest.csproj
new file mode 100644
index 0000000..ae654fa
--- /dev/null
+++ b/src/SampleApps/ColorSendTest/ColorSendTest.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SampleApps/ColorSendTest/Program.cs b/src/SampleApps/ColorSendTest/Program.cs
new file mode 100644
index 0000000..1acf481
--- /dev/null
+++ b/src/SampleApps/ColorSendTest/Program.cs
@@ -0,0 +1,353 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.Linq;
+using System.Threading.Tasks;
+using LifxNet;
+using Newtonsoft.Json;
+
+namespace ColorSendTest {
+ class Program {
+ private static LifxClient _client;
+ private static List _devicesBulb;
+ private static List _devicesMulti;
+ private static List _devicesMultiV2;
+ private static List _devicesTile;
+ private static List _devicesSwitch;
+
+ static async Task Main(string[] args) {
+ var tr1 = new TextWriterTraceListener(Console.Out);
+ _devicesBulb = new List();
+ _devicesMulti = new List();
+ _devicesMultiV2 = new List();
+ _devicesTile = new List();
+ _devicesSwitch = new List();
+ _client = LifxClient.CreateAsync().Result;
+ _client.DeviceDiscovered += ClientDeviceDiscovered;
+ _client.DeviceLost += ClientDeviceLost;
+ Console.WriteLine("Enumerating devices, please wait 15 seconds...");
+ _client.StartDeviceDiscovery();
+ await Task.Delay(5000);
+ _client.StopDeviceDiscovery();
+ Trace.Listeners.Add(tr1);
+ Console.WriteLine("Please select a device type to test (Enter a number):");
+ if (_devicesBulb.Count > 0) {
+ Console.WriteLine("1: Bulbs");
+ }
+
+ if (_devicesMulti.Count > 0) {
+ Console.WriteLine("2: Multi Zone V1");
+ }
+
+ if (_devicesMultiV2.Count > 0) {
+ Console.WriteLine("3: Multi Zone V2");
+ }
+
+ if (_devicesTile.Count > 0) {
+ Console.WriteLine("4: Tiles");
+ }
+
+ if (_devicesSwitch.Count > 0) {
+ Console.WriteLine("5: Switch");
+ }
+
+ var selection = int.Parse(Console.ReadLine() ?? "0");
+ switch (selection) {
+ case 1:
+ Console.WriteLine("Flashing bulbs on and off.");
+ await FlashBulbs();
+ break;
+ case 2:
+ Console.WriteLine("Flashing multizone v1 devices on and off.");
+ await FlashMultizone();
+ break;
+ case 3:
+ Console.WriteLine("Flashing multizone v2 devices on and off.");
+ await FlashMultizoneV2();
+ break;
+ case 4:
+ Console.WriteLine("Flashing tile devices on and off.");
+ await FlashTiles();
+ break;
+ case 5:
+ Console.WriteLine("Toggling switches is not enabled yet.");
+ FlashSwitches();
+ break;
+ }
+
+ Console.WriteLine("All done!");
+ Console.ReadKey();
+ }
+
+ private static async Task FlashBulbs() {
+ // Save our existing states
+ var stateList = new List();
+ var red = new LifxColor(255, 0, 0);
+ var black = new LifxColor(0, 0, 0);
+ foreach (var b in _devicesBulb) {
+ var bulb = (LightBulb) b;
+ var state = await _client.GetLightStateAsync(bulb);
+ stateList.Add(state);
+ await _client.SetPowerAsync(b, 1);
+ await _client.SetBrightnessAsync(bulb, 255, TimeSpan.Zero);
+ }
+
+ Console.WriteLine($"Flashing {_devicesBulb.Count} bulbs.");
+ foreach (var bulb in _devicesBulb.Cast()) {
+ _client.SetColorAsync(bulb, red, 2700).ConfigureAwait(false);
+ }
+
+ await Task.Delay(1000);
+ foreach (var bulb in _devicesBulb.Cast()) {
+ _client.SetColorAsync(bulb, black, 2700).ConfigureAwait(false);
+ }
+
+ await Task.Delay(500);
+
+ foreach (var bulb in _devicesBulb.Cast()) {
+ _client.SetColorAsync(bulb, red, 2700).ConfigureAwait(false);
+ }
+
+ await Task.Delay(1000);
+ foreach (var bulb in _devicesBulb.Cast()) {
+ _client.SetColorAsync(bulb, black, 2700).ConfigureAwait(false);
+ }
+
+ await Task.Delay(500);
+ // Set them to red
+ var idx = 0;
+ Console.WriteLine("Restoring bulb states.");
+ foreach (var b in _devicesBulb) {
+ var bulb = (LightBulb) b;
+ var state = stateList[idx];
+ await _client.SetBrightnessAsync(bulb, state.Brightness, TimeSpan.Zero);
+ await _client.SetPowerAsync(bulb, state.IsOn ? 1 : 0);
+ idx++;
+ }
+ }
+
+ private static async Task FlashMultizone() {
+ var stateList = new List();
+ var responses = new List();
+ foreach (var m in _devicesMulti) {
+ var state = await _client.GetPowerAsync(m);
+ stateList.Add(state);
+ var zoneState = await _client.GetColorZonesAsync(m,0,8);
+ responses.Add(zoneState);
+ _client.SetPowerAsync(m, 1).ConfigureAwait(false);
+ }
+
+ var idx = 0;
+ foreach (var m in _devicesMulti) {
+ var state = responses[idx];
+ var count = state.Count;
+ var start = state.Index;
+ var total = start - count;
+ for (var i = start; i < count; i++) {
+ var pi = i * 1.0f;
+ var progress = (start - pi) / total;
+ var apply = i == count - 1;
+ _client.SetColorZonesAsync(m, i, i, Rainbow(progress), TimeSpan.Zero, apply);
+ }
+ idx++;
+ }
+
+ await Task.Delay(2000);
+
+ idx = 0;
+ Debug.WriteLine("Setting v1 multi to rainbow!");
+ var black = new LifxColor(0, 0, 0);
+ foreach (var m in _devicesMulti) {
+ var state = responses[idx];
+ var count = state.Count;
+ var start = state.Index;
+ var total = start - count;
+ for (var i = start; i < count; i++) {
+ _client.SetColorZonesAsync(m, i, i, black, TimeSpan.Zero, true);
+ }
+ idx++;
+ }
+
+ idx = 0;
+ Debug.WriteLine("Setting v1 multi to black/disabling.");
+ foreach (var m in _devicesMulti) {
+ var power = stateList[idx];
+ if (power == 0) {
+ _client.SetPowerAsync(m, power);
+ }
+ }
+ }
+
+ private static async Task FlashMultizoneV2() {
+ var stateList = new List();
+ var responses = new List();
+ foreach (var m in _devicesMulti) {
+ var state = await _client.GetPowerAsync(m);
+ stateList.Add(state);
+ var zoneState = await _client.GetExtendedColorZonesAsync(m);
+ responses.Add(zoneState);
+ _client.SetPowerAsync(m, 1);
+ }
+ Debug.WriteLine("Setting devices to rainbow!");
+ var idx = 0;
+ foreach (var m in _devicesMulti) {
+ var state = responses[idx];
+ var count = state.Count;
+ var start = state.Index;
+ var total = start - count;
+ var colors = new List();
+
+ for (var i = start; i < count; i++) {
+ var pi = i * 1.0f;
+ var progress = (start - pi) / total;
+ colors.Add(Rainbow(progress));
+ }
+ _client.SetExtendedColorZonesAsync(m, TimeSpan.Zero, start, colors, true);
+ idx++;
+ }
+
+ await Task.Delay(2000);
+ Debug.WriteLine("Setting v2 to black.");
+
+ idx = 0;
+ var black = new LifxColor(0, 0, 0);
+ foreach (var m in _devicesMulti) {
+ var state = responses[idx];
+ var count = state.Count;
+ var start = state.Index;
+ var colors = new List();
+ for (var i = start; i < count; i++) {
+ colors.Add(black);
+ }
+ _client.SetExtendedColorZonesAsync(m, TimeSpan.Zero, start, colors,true);
+ idx++;
+ }
+
+ idx = 0;
+ Debug.WriteLine("Resetting v2 multizone.");
+
+ foreach (var m in _devicesMulti) {
+ var power = stateList[idx];
+ if (power == 0) {
+ _client.SetPowerAsync(m, power);
+ }
+ }
+ }
+
+ private static async Task FlashTiles() {
+ var chains = new List();
+ foreach (var t in _devicesTile) {
+ var state = _client.GetDeviceChainAsync(t).Result;
+ chains.Add(state);
+ _client.SetPowerAsync(t, 1);
+ }
+
+ var idx = 0;
+ Debug.WriteLine("Rainbowing tiles!");
+
+ foreach (var t in _devicesTile) {
+ var state = chains[idx];
+ var tidx = 0;
+ var colors = new List();
+ for (var c = 0; c < 64; c++) {
+ var pi = c * 1.0f;
+ var progress = pi / 64;
+ colors.Add(Rainbow(progress));
+ }
+ for (var i = state.StartIndex; i < state.TotalCount; i++) {
+ _client.SetTileState64Async(t, i, 64, 1000, colors.ToArray());
+ }
+ idx++;
+ }
+
+ await Task.Delay(2000);
+
+ idx = 0;
+ Debug.WriteLine("Turning off tiles.");
+ foreach (var t in _devicesTile) {
+ var state = chains[idx];
+ var colors = new List();
+ for (var c = 0; c < 64; c++) {
+ colors.Add(new LifxColor(0,0,0));
+ }
+ for (var i = state.StartIndex; i < state.TotalCount; i++) {
+ _client.SetTileState64Async(t, i, 64, 1000, colors.ToArray());
+ }
+
+ _client.SetPowerAsync(t, 0);
+ idx++;
+ }
+ }
+
+ private static void FlashSwitches() {
+
+ }
+
+ private static LifxColor Rainbow(float progress) {
+ Console.WriteLine("Progress is " + progress);
+ var div = Math.Abs(progress % 1) * 6;
+ var ascending = (int) (div % 1 * 255);
+ var descending = 255 - ascending;
+ var output = (int) div switch {
+ 0 => Color.FromArgb(255, ascending, 0),
+ 1 => Color.FromArgb(descending, 255, 0),
+ 2 => Color.FromArgb(0, 255, ascending),
+ 3 => Color.FromArgb(0, descending, 255),
+ 4 => Color.FromArgb(ascending, 0, 255),
+ _ => Color.FromArgb(255, 0, descending)
+ };
+ return new LifxColor(output);
+ }
+
+ private static void ClientDeviceLost(object sender, LifxClient.DeviceDiscoveryEventArgs e)
+ {
+ Console.WriteLine("Device lost");
+ }
+
+ private static async void ClientDeviceDiscovered(object sender, LifxClient.DeviceDiscoveryEventArgs e)
+ {
+ Console.WriteLine($"Device {e.Device.MacAddressName} found @ {e.Device.HostName}");
+ var version = await _client.GetDeviceVersionAsync(e.Device);
+ var added = false;
+ // Multi-zone devices
+ if (version.Product == 31 || version.Product == 32 || version.Product == 38) {
+ var extended = false;
+ // If new Z-LED or Beam, check if FW supports "extended" commands.
+ if (version.Product == 32 || version.Product == 38) {
+ if (version.Version >= 1532997580) {
+ extended = true;
+ }
+ }
+
+ if (extended) {
+ added = true;
+ Console.WriteLine("Adding V2 Multi zone Device.");
+ _devicesMultiV2.Add(e.Device);
+ } else {
+ added = true;
+ Console.WriteLine("Adding V1 Multi zone Device.");
+ _devicesMulti.Add(e.Device);
+ }
+ }
+
+ // Tile
+ if (version.Product == 55) {
+ added = true;
+ Console.WriteLine("Adding Tile Device");
+ _devicesTile.Add(e.Device);
+ }
+ // Switch
+ if (version.Product == 70) {
+ added = true;
+ Console.WriteLine("Adding Switch Device.");
+ _devicesSwitch.Add(e.Device);
+ }
+
+ if (!added) {
+ Console.WriteLine("Adding Bulb.");
+ _devicesBulb.Add(e.Device);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SampleApps/LifxEmulator/LifxEmulator.csproj b/src/SampleApps/LifxEmulator/LifxEmulator.csproj
new file mode 100644
index 0000000..d7e88b2
--- /dev/null
+++ b/src/SampleApps/LifxEmulator/LifxEmulator.csproj
@@ -0,0 +1,76 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {35655608-57DD-43AB-9D2E-81B329CC6473}
+ Exe
+ Properties
+ LifxEmulator
+ LifxEmulator
+ v4.7.2
+ 512
+ 8
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\..\packages\Colorful.Console.1.2.15\lib\net461\Colorful.Console.dll
+ True
+
+
+
+ ..\..\packages\Newtonsoft.Json.13.0.1-beta1\lib\net45\Newtonsoft.Json.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {9ecd8c00-1c8c-4abb-b327-525d9e719c20}
+ LifxNet
+
+
+
+
+
+
+
+
+
diff --git a/src/SampleApps/LifxEmulator/LifxPacket.cs b/src/SampleApps/LifxEmulator/LifxPacket.cs
new file mode 100644
index 0000000..fff3fbb
--- /dev/null
+++ b/src/SampleApps/LifxEmulator/LifxPacket.cs
@@ -0,0 +1,98 @@
+using System;
+using System.IO;
+
+namespace LifxEmulator {
+ internal abstract class LifxPacket {
+ private byte[] _payload;
+ private ushort _type;
+
+ protected LifxPacket(ushort type, byte[] payload) {
+ _type = type;
+ _payload = payload;
+ }
+
+ internal byte[] Payload {
+ get { return _payload; }
+ }
+
+ internal ushort Type {
+ get { return _type; }
+ }
+
+ protected LifxPacket(ushort type, object[] data) {
+ _type = type;
+ using var ms = new MemoryStream();
+ StreamWriter bw = new StreamWriter(ms);
+ foreach (var obj in data) {
+ switch (obj) {
+ case byte b:
+ bw.Write(b);
+ break;
+ case byte[] bytes:
+ bw.Write(bytes);
+ break;
+ case ushort @ushort:
+ bw.Write(@ushort);
+ break;
+ case uint u:
+ bw.Write(u);
+ break;
+ default:
+ throw new NotImplementedException();
+ }
+ }
+
+ _payload = ms.ToArray();
+ }
+
+ public static LifxPacket FromByteArray(byte[] data) {
+ // preambleFields = [
+ // { name: "size" , type:type.uint16_le },
+ // { name: "protocol" , type:type.uint16_le },
+ // { name: "reserved1" , type:type.byte4 } ,
+ // { name: "bulbAddress", type:type.byte6 } ,
+ // { name: "reserved2" , type:type.byte2 } ,
+ // { name: "site" , type:type.byte6 } ,
+ // { name: "reserved3" , type:type.byte2 } ,
+ // { name: "timestamp" , type:type.uint64 } ,
+ // { name: "packetType" , type:type.uint16_le },
+ // { name: "reserved4" , type:type.byte2 } ,
+ // ];
+ MemoryStream ms = new MemoryStream(data);
+ var br = new BinaryReader(ms);
+ //Header
+ ushort len = br.ReadUInt16(); //ReverseBytes(br.ReadUInt16()); //size uint16
+ ushort protocol = br.ReadUInt16(); // ReverseBytes(br.ReadUInt16()); //origin = 0
+ var identifier = br.ReadUInt32();
+ byte[] bulbAddress = br.ReadBytes(6);
+ byte[] reserved2 = br.ReadBytes(2);
+ byte[] site = br.ReadBytes(6);
+ byte[] reserved3 = br.ReadBytes(2);
+ ulong timestamp = br.ReadUInt64();
+ ushort packetType = br.ReadUInt16(); // ReverseBytes(br.ReadUInt16());
+ byte[] reserved4 = br.ReadBytes(2);
+ byte[] payload = { };
+ if (len > 0) {
+ payload = br.ReadBytes(len);
+ }
+
+ LifxPacket packet = new UnknownPacket(packetType, payload, bulbAddress, site) {
+ TimeStamp = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(timestamp),
+ };
+ //packet.Identifier = identifier;
+ return packet;
+ }
+
+ private class UnknownPacket : LifxPacket {
+ public UnknownPacket(ushort packetType, byte[] payload, byte[] bulbAddress, byte[] site) : base(packetType,
+ payload) {
+ BulbAddress = bulbAddress;
+ Site = site;
+ }
+
+ public byte[] BulbAddress { get; }
+ public DateTime TimeStamp { get; set; }
+ public byte[] Site { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SampleApps/LifxEmulator/LifxResponses.cs b/src/SampleApps/LifxEmulator/LifxResponses.cs
new file mode 100644
index 0000000..7d6f4c8
--- /dev/null
+++ b/src/SampleApps/LifxEmulator/LifxResponses.cs
@@ -0,0 +1,414 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.Text;
+using LifxNet;
+using Newtonsoft.Json;
+
+namespace LifxEmulator {
+ ///
+ /// Base class for LIFX response types
+ ///
+ public abstract class LifxResponse {
+ internal static LifxResponse Create(FrameHeader header, MessageType type, uint source, Payload payload, int deviceVersion) {
+ payload.Reset();
+ switch (type) {
+ case MessageType.DeviceGetService:
+ type = MessageType.DeviceStateService;
+ return new StateServiceResponse(header, type, source);
+ case MessageType.DeviceEchoRequest:
+ type = MessageType.DeviceEchoResponse;
+ return new EchoResponse(header, type, payload, source);
+ case MessageType.DeviceGetInfo:
+ type = MessageType.DeviceStateInfo;
+ return new StateInfoResponse(header, type, source);
+ case MessageType.LightGet:
+ type = MessageType.LightState;
+ return new LightStateResponse(header, type, source);
+ case MessageType.DeviceGetVersion:
+ type = MessageType.DeviceStateVersion;
+ return new StateVersionResponse(header, type, source, deviceVersion);
+ case MessageType.DeviceGetHostFirmware:
+ type = MessageType.DeviceStateHostFirmware;
+ return new StateHostFirmwareResponse(header, type, source, deviceVersion);
+ case MessageType.GetExtendedColorZones:
+ type = MessageType.StateExtendedColorZones;
+ return new StateExtendedColorZonesResponse(header, type, source);
+ case MessageType.GetColorZones:
+ type = MessageType.StateMultiZone;
+ return new StateMultiZoneResponse(header, type, source);
+ case MessageType.GetDeviceChain:
+ type = MessageType.StateDeviceChain;
+ return new StateDeviceChainResponse(header, type, source);
+ case MessageType.GetRelayPower:
+ type = MessageType.StateRelayPower;
+ return new StateRelayPowerResponse(header, type, source);
+ case MessageType.DeviceGetPower:
+ type = MessageType.DeviceStatePower;
+ return new StatePowerResponse(header, type, source);
+ case MessageType.DeviceSetPower:
+ type = MessageType.DeviceAcknowledgement;
+ return new AcknowledgementResponse(header, type, source);
+ default:
+ type = MessageType.DeviceAcknowledgement;
+ return new AcknowledgementResponse(header, type, source);
+ }
+ }
+
+ internal LifxResponse(FrameHeader header, MessageType type, uint source) {
+ Header = header;
+ Type = type;
+ Source = source;
+ }
+
+ internal FrameHeader Header { get; }
+ internal Payload Payload { get; set; }
+ internal MessageType Type { get; }
+ internal uint Source { get; }
+ }
+
+ ///
+ /// Response to GetService message.
+ /// Provides the device Service and port.
+ /// If the Service is temporarily unavailable, then the port value will be 0.
+ ///
+ internal class StateServiceResponse : LifxResponse {
+ internal StateServiceResponse(FrameHeader header, MessageType type, uint source) : base(
+ header, type, source) {
+ Service = 1;
+ Port = 56700;
+ Payload = new Payload(new object[]{Service, Port});
+ }
+
+ private byte Service { get; }
+ private ulong Port { get; }
+ }
+
+ ///
+ /// Response to any message sent with ack_required set to 1.
+ ///
+ internal class AcknowledgementResponse : LifxResponse {
+ internal AcknowledgementResponse(FrameHeader header, MessageType type, uint source) : base(
+ header, type, source) {
+ }
+ }
+
+ ///
+ /// Get the list of colors currently being displayed by zones
+ ///
+ public class StateMultiZoneResponse : LifxResponse {
+ internal StateMultiZoneResponse(FrameHeader header, MessageType type, uint source) : base(
+ header, type, source) {
+ Count = 8;
+ Index = 0;
+ Colors = new LifxColor[Count];
+ for (var i = Index; i < Count; i++) {
+ Colors[i] = new LifxColor(255,0,0);
+ }
+
+ var args = new List