Refactored to support multiple Device
implementations.
This commit is contained in:
parent
6b5fb893ef
commit
2b64e1f655
12
Cargo.toml
12
Cargo.toml
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "luxafor"
|
name = "luxafor"
|
||||||
description = "Library, and CLI, for Luxafor lights via webhooks."
|
description = "Library, and CLI, for Luxafor lights via webhooks."
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
authors = ["Simon Johnston <johnstonskj@gmail.com>"]
|
authors = ["Simon Johnston <johnstonskj@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@ -14,7 +14,10 @@ targets = ["x86_64-unknown-linux-gnu"]
|
|||||||
all-features = true
|
all-features = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
command-line = ["pretty_env_logger", "structopt"]
|
default = ["webhook"]
|
||||||
|
usb = ["hidapi"]
|
||||||
|
webhook = ["reqwest"]
|
||||||
|
command-line = ["pretty_env_logger", "structopt", "usb", "webhook"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "lux"
|
name = "lux"
|
||||||
@ -24,8 +27,9 @@ required-features = ["command-line"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.11"
|
log = "0.4.11"
|
||||||
error-chain = "0.12.2"
|
error-chain = "0.12.2"
|
||||||
reqwest = { version = "0.10", features = ["blocking", "json"] }
|
|
||||||
|
|
||||||
#[feature-dependencies]
|
#[feature-dependencies]
|
||||||
|
hidapi = { version = "~0.4", optional = true }
|
||||||
|
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||||
|
reqwest = { version = "0.10", features = ["blocking"], optional = true }
|
||||||
structopt = { version = "0.3.14", optional = true }
|
structopt = { version = "0.3.14", optional = true }
|
||||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
|
17
README.md
17
README.md
@ -1,6 +1,6 @@
|
|||||||
# Crate luxafor
|
# Crate luxafor
|
||||||
|
|
||||||
Library, and CLI, for [Luxafor](https://luxafor.com/products/) lights via webhooks.
|
Library, and CLI, for [Luxafor](https://luxafor.com/products/) lights via webhooks or USB.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@ -12,7 +12,7 @@ Library, and CLI, for [Luxafor](https://luxafor.com/products/) lights via webhoo
|
|||||||
This has been tested with the USB connected [flag](https://luxafor.com/flag-usb-busylight-availability-indicator/)
|
This has been tested with the USB connected [flag](https://luxafor.com/flag-usb-busylight-availability-indicator/)
|
||||||
as well as the [Bluetooth](https://luxafor.com/bluetooth-busy-light-availability-indicator/) lights.
|
as well as the [Bluetooth](https://luxafor.com/bluetooth-busy-light-availability-indicator/) lights.
|
||||||
|
|
||||||
# Examples
|
## Examples
|
||||||
|
|
||||||
The following shows the command line tool setting the color to red.
|
The following shows the command line tool setting the color to red.
|
||||||
|
|
||||||
@ -36,9 +36,20 @@ The following shows the command line tool turning the light off.
|
|||||||
INFO luxafor > call successful
|
INFO luxafor > call successful
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* **command-line**; provides the command line tool `lux`, it is not on by default for library clients.
|
||||||
|
* **usb**; provides access to USB connected devices.
|
||||||
|
* **webhook** (default); provides access to USB, or Bluetooth, devices via webhooks.
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
|
|
||||||
|
**Version 0.2.0**
|
||||||
|
|
||||||
|
* Refactored to provide a new `Device` trait
|
||||||
|
* Implemented the trait for webhook connected lights
|
||||||
|
* Added a new implementation for HID connected lights
|
||||||
|
|
||||||
**Version 0.1.0**
|
**Version 0.1.0**
|
||||||
|
|
||||||
* Initial commit, supports flag and bluetooth lights.
|
* Initial commit, supports flag and bluetooth lights.
|
||||||
@ -46,4 +57,4 @@ The following shows the command line tool turning the light off.
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
TBD
|
* The webhook API is not as rich as the USB, need to find a way to manage this.
|
@ -1,7 +1,9 @@
|
|||||||
|
#[allow(unused_imports)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
use luxafor::{set_pattern, set_solid_color, turn_off, DeviceID, Pattern, SolidColor};
|
use luxafor::usb_hid::USBDeviceDiscovery;
|
||||||
|
use luxafor::{webhook, Device, Pattern, SolidColor};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
@ -14,7 +16,7 @@ pub(crate) struct CommandLine {
|
|||||||
|
|
||||||
/// The device identifier
|
/// The device identifier
|
||||||
#[structopt(long, short, env = "LUX_DEVICE")]
|
#[structopt(long, short, env = "LUX_DEVICE")]
|
||||||
device: DeviceID,
|
device: String,
|
||||||
|
|
||||||
#[structopt(subcommand)]
|
#[structopt(subcommand)]
|
||||||
cmd: SubCommand,
|
cmd: SubCommand,
|
||||||
@ -37,7 +39,6 @@ pub(crate) enum SubCommand {
|
|||||||
/// Set the light to a to a pre-defined pattern
|
/// Set the light to a to a pre-defined pattern
|
||||||
Pattern {
|
Pattern {
|
||||||
/// The pattern to set
|
/// The pattern to set
|
||||||
#[structopt(long, short)]
|
|
||||||
pattern: Pattern,
|
pattern: Pattern,
|
||||||
},
|
},
|
||||||
/// Turn the light off
|
/// Turn the light off
|
||||||
@ -58,11 +59,23 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
})
|
})
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
if args.device == "usb" {
|
||||||
|
let discovery = USBDeviceDiscovery::new()?;
|
||||||
|
let device = discovery.device()?;
|
||||||
|
debug!("USB device: '{}'", device.id());
|
||||||
|
set_lights(args, device)
|
||||||
|
} else {
|
||||||
|
let device_id = args.device.clone();
|
||||||
|
set_lights(args, webhook::new_device_for(&device_id)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_lights(args: CommandLine, device: impl Device) -> Result<(), Box<dyn Error>> {
|
||||||
match args.cmd {
|
match args.cmd {
|
||||||
SubCommand::Solid { color } => set_solid_color(args.device, color, false),
|
SubCommand::Solid { color } => device.set_solid_color(color, false),
|
||||||
SubCommand::Blink { color } => set_solid_color(args.device, color, true),
|
SubCommand::Blink { color } => device.set_solid_color(color, true),
|
||||||
SubCommand::Pattern { pattern } => set_pattern(args.device, pattern),
|
SubCommand::Pattern { pattern } => device.set_pattern(pattern),
|
||||||
SubCommand::Off => turn_off(args.device),
|
SubCommand::Off => device.turn_off(),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
194
src/lib.rs
194
src/lib.rs
@ -1,5 +1,7 @@
|
|||||||
/*!
|
/*!
|
||||||
Library, and CLI, for [Luxafor](https://luxafor.com/products/) lights via webhooks. This has been
|
Library, and CLI, for [Luxafor](https://luxafor.com/products/) lights via webhooks or USB.
|
||||||
|
|
||||||
|
This has been
|
||||||
tested with the USB connected [flag](https://luxafor.com/flag-usb-busylight-availability-indicator/)
|
tested with the USB connected [flag](https://luxafor.com/flag-usb-busylight-availability-indicator/)
|
||||||
as well as the [Bluetooth](https://luxafor.com/bluetooth-busy-light-availability-indicator/) lights.
|
as well as the [Bluetooth](https://luxafor.com/bluetooth-busy-light-availability-indicator/) lights.
|
||||||
|
|
||||||
@ -27,6 +29,18 @@ The following shows the command line tool turning the light off.
|
|||||||
INFO luxafor > call successful
|
INFO luxafor > call successful
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The following shows the how to set USB connected lights.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ lux -d usb solid red
|
||||||
|
```
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
* **command-line**; provides the command line tool `lux`, it is not on by default for library clients.
|
||||||
|
* **usb**; provides access to USB connected devices.
|
||||||
|
* **webhook** (default); provides access to USB, or Bluetooth, devices via webhooks.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#![warn(
|
#![warn(
|
||||||
@ -38,7 +52,7 @@ The following shows the command line tool turning the light off.
|
|||||||
trivial_numeric_casts,
|
trivial_numeric_casts,
|
||||||
// ---------- Public
|
// ---------- Public
|
||||||
missing_debug_implementations,
|
missing_debug_implementations,
|
||||||
//missing_docs,
|
missing_docs,
|
||||||
unreachable_pub,
|
unreachable_pub,
|
||||||
// ---------- Unsafe
|
// ---------- Unsafe
|
||||||
unsafe_code,
|
unsafe_code,
|
||||||
@ -52,10 +66,10 @@ The following shows the command line tool turning the light off.
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate error_chain;
|
extern crate error_chain;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
use reqwest::blocking::Client;
|
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
@ -63,12 +77,6 @@ use std::str::FromStr;
|
|||||||
// Public Types
|
// Public Types
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
///
|
|
||||||
/// This wraps a simple string and ensures it only contains valid characters.
|
|
||||||
///
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct DeviceID(String);
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// A color that the light can be set to.
|
/// A color that the light can be set to.
|
||||||
///
|
///
|
||||||
@ -89,7 +97,14 @@ pub enum SolidColor {
|
|||||||
/// A preset color
|
/// A preset color
|
||||||
Magenta,
|
Magenta,
|
||||||
/// A custom color using standard RGB values
|
/// A custom color using standard RGB values
|
||||||
Custom { red: u8, green: u8, blue: u8 },
|
Custom {
|
||||||
|
/// The _red_ channel
|
||||||
|
red: u8,
|
||||||
|
/// The _green_ channel
|
||||||
|
green: u8,
|
||||||
|
/// The _blue_ channel
|
||||||
|
blue: u8,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -112,112 +127,40 @@ pub enum Pattern {
|
|||||||
Synthetic,
|
Synthetic,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
///
|
||||||
// Private Types
|
/// Trait describing a device identifier, basically you just need to be able to `to_string()` it.
|
||||||
// ------------------------------------------------------------------------------------------------
|
///
|
||||||
|
pub trait DeviceIdentifier: Display {}
|
||||||
const API_V1: &str = "https://api.luxafor.com/webhook/v1/actions";
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
|
||||||
// Public Functions
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Turn the light off.
|
/// A trait implemented by different access methods to control a light.
|
||||||
///
|
///
|
||||||
pub fn turn_off(device: DeviceID) -> error::Result<()> {
|
pub trait Device {
|
||||||
set_solid_color(
|
///
|
||||||
device,
|
/// Return the identifier for the device.
|
||||||
SolidColor::Custom {
|
///
|
||||||
red: 00,
|
fn id(&self) -> &dyn DeviceIdentifier;
|
||||||
green: 00,
|
|
||||||
blue: 00,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Set the color, and blink status, of the light.
|
/// Turn the light off.
|
||||||
///
|
///
|
||||||
pub fn set_solid_color(device: DeviceID, color: SolidColor, blink: bool) -> error::Result<()> {
|
fn turn_off(&self) -> error::Result<()>;
|
||||||
info!("Setting the color of device '{}' to {}", device, color);
|
|
||||||
|
|
||||||
let body = if let SolidColor::Custom {
|
///
|
||||||
red: _,
|
/// Set the color, and blink status, of the light.
|
||||||
green: _,
|
///
|
||||||
blue: _,
|
fn set_solid_color(&self, color: SolidColor, blink: bool) -> error::Result<()>;
|
||||||
} = color
|
|
||||||
{
|
|
||||||
r#"{
|
|
||||||
"userId": "DID",
|
|
||||||
"actionFields":{
|
|
||||||
"color": "custom",
|
|
||||||
"custom_color": "COLOR"
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.replace("DID", &device.to_string())
|
|
||||||
.replace("COLOR", &color.to_string())
|
|
||||||
} else {
|
|
||||||
r#"{
|
|
||||||
"userId": "DID",
|
|
||||||
"actionFields":{
|
|
||||||
"color": "COLOR"
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.replace("DID", &device.to_string())
|
|
||||||
.replace("COLOR", &color.to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = &format!("{}/{}", API_V1, if blink { "blink" } else { "solid_color" });
|
///
|
||||||
|
/// Set the pattern displayed by the light.
|
||||||
send_request(url, body)
|
///
|
||||||
}
|
fn set_pattern(&self, pattern: Pattern) -> error::Result<()>;
|
||||||
|
|
||||||
///
|
|
||||||
/// Set the pattern displayed by the light.
|
|
||||||
///
|
|
||||||
pub fn set_pattern(device: DeviceID, pattern: Pattern) -> error::Result<()> {
|
|
||||||
info!("Setting the pattern of device '{}' to {}", device, pattern);
|
|
||||||
|
|
||||||
let body = r#"{
|
|
||||||
"userId": "DID",
|
|
||||||
"actionFields":{
|
|
||||||
"pattern": "PATTERN"
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.replace("DID", &device.to_string())
|
|
||||||
.replace("PATTERN", &pattern.to_string());
|
|
||||||
|
|
||||||
let url = &format!("{}/{}", API_V1, "pattern");
|
|
||||||
|
|
||||||
send_request(url, body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
// Implementations
|
// Implementations
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
impl Display for DeviceID {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for DeviceID {
|
|
||||||
type Err = error::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
if !s.is_empty() && s.chars().all(|c| c.is_ascii_hexdigit()) {
|
|
||||||
Ok(Self(s.to_string()))
|
|
||||||
} else {
|
|
||||||
Err(error::ErrorKind::InvalidDeviceID.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
impl Display for SolidColor {
|
impl Display for SolidColor {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
fn to_hex(v: &u8) -> String {
|
fn to_hex(v: &u8) -> String {
|
||||||
@ -310,37 +253,15 @@ impl FromStr for Pattern {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
|
||||||
// Private Functions
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn send_request(api: &str, body: String) -> error::Result<()> {
|
|
||||||
debug!("Sending to: {}", api);
|
|
||||||
debug!("Sending data: {:?}", body);
|
|
||||||
|
|
||||||
let client = Client::new();
|
|
||||||
let result = client
|
|
||||||
.post(api)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.body(body)
|
|
||||||
.send()?;
|
|
||||||
|
|
||||||
if result.status().is_success() {
|
|
||||||
info!("call successful");
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
let status_code = result.status().as_u16();
|
|
||||||
error!("call failed");
|
|
||||||
error!("{:?}", result.text());
|
|
||||||
Err(error::ErrorKind::UnexpectedError(status_code).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
// Modules
|
// Modules
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
mod error {
|
///
|
||||||
|
/// Error handling types.
|
||||||
|
///
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub mod error {
|
||||||
error_chain! {
|
error_chain! {
|
||||||
errors {
|
errors {
|
||||||
#[doc("The color value supplied was not recognized")]
|
#[doc("The color value supplied was not recognized")]
|
||||||
@ -358,6 +279,11 @@ mod error {
|
|||||||
description("The provided device ID was incorrectly formatted")
|
description("The provided device ID was incorrectly formatted")
|
||||||
display("The provided device ID was incorrectly formatted")
|
display("The provided device ID was incorrectly formatted")
|
||||||
}
|
}
|
||||||
|
#[doc("No device was discovered, or the ID did not resolve to a device")]
|
||||||
|
DeviceNotFound {
|
||||||
|
description("No device was discovered, or the ID did not resolve to a device")
|
||||||
|
display("No device was discovered, or the ID did not resolve to a device")
|
||||||
|
}
|
||||||
#[doc("The server indicated an invalid request")]
|
#[doc("The server indicated an invalid request")]
|
||||||
InvalidRequest {
|
InvalidRequest {
|
||||||
description("The server indicated an invalid request")
|
description("The server indicated an invalid request")
|
||||||
@ -376,3 +302,9 @@ mod error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "usb")]
|
||||||
|
pub mod usb_hid;
|
||||||
|
|
||||||
|
#[cfg(feature = "webhook")]
|
||||||
|
pub mod webhook;
|
||||||
|
230
src/usb_hid.rs
Normal file
230
src/usb_hid.rs
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
/*!
|
||||||
|
Implementation of the Device trait for USB connected lights.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::{Device, DeviceIdentifier, Pattern, SolidColor};
|
||||||
|
use hidapi::{HidApi, HidDevice};
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Public Types
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
///
|
||||||
|
/// This enables the discovery of the device using the USB HID descriptor.
|
||||||
|
///
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct USBDeviceDiscovery {
|
||||||
|
hid_api: HidApi,
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// The device identifier for a USB connected light.
|
||||||
|
///
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct USBDeviceID(String);
|
||||||
|
|
||||||
|
///
|
||||||
|
/// The device implementation for a USB connected light.
|
||||||
|
///
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct USBDevice<'a> {
|
||||||
|
hid_device: HidDevice<'a>,
|
||||||
|
id: USBDeviceID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// API Constants
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const LUXAFOR_VENDOR_ID: u16 = 0x04d8;
|
||||||
|
const LUXAFOR_PRODUCT_ID: u16 = 0xf372;
|
||||||
|
|
||||||
|
const MODE_SOLID: u8 = 1;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const MODE_FADE: u8 = 2;
|
||||||
|
const MODE_STROBE: u8 = 3;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const MODE_WAVE: u8 = 4;
|
||||||
|
const MODE_PATTERN: u8 = 6;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const LED_FRONT_TOP: u8 = 1;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const LED_FRONT_MIDDLE: u8 = 2;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const LED_FRONT_BOTTOM: u8 = 3;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const LED_BACK_TOP: u8 = 4;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const LED_BACK_MIDDLE: u8 = 5;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const LED_BACK_BOTTOM: u8 = 6;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const LED_FRONT_ALL: u8 = 65;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const LED_BACK_ALL: u8 = 66;
|
||||||
|
const LED_ALL: u8 = 255;
|
||||||
|
|
||||||
|
const PATTERN_LUXAFOR: u8 = 1;
|
||||||
|
const PATTERN_RANDOM_1: u8 = 2;
|
||||||
|
const PATTERN_RANDOM_2: u8 = 3;
|
||||||
|
const PATTERN_RANDOM_3: u8 = 4;
|
||||||
|
const PATTERN_RANDOM_4: u8 = 6;
|
||||||
|
const PATTERN_RANDOM_5: u8 = 7;
|
||||||
|
const PATTERN_POLICE: u8 = 5;
|
||||||
|
const PATTERN_RAINBOW_WAVE: u8 = 8;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Public Functions
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Implementations
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
impl Display for USBDeviceID {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceIdentifier for USBDeviceID {}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
impl USBDeviceDiscovery {
|
||||||
|
///
|
||||||
|
/// Construct a new discovery object, this initializes the USB HID interface and thus can fail.
|
||||||
|
///
|
||||||
|
pub fn new() -> crate::error::Result<Self> {
|
||||||
|
let hid_api = HidApi::new()?;
|
||||||
|
Ok(Self { hid_api })
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Return a device, if found, that corresponds to a Luxafor light.
|
||||||
|
///
|
||||||
|
pub fn device(&self) -> crate::error::Result<USBDevice<'_>> {
|
||||||
|
let result = self.hid_api.open(LUXAFOR_VENDOR_ID, LUXAFOR_PRODUCT_ID);
|
||||||
|
match result {
|
||||||
|
Ok(hid_device) => USBDevice::new(hid_device),
|
||||||
|
Err(err) => {
|
||||||
|
error!("Could not open HID device: {:?}", err);
|
||||||
|
Err(crate::error::ErrorKind::DeviceNotFound.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
impl<'a> Device for USBDevice<'a> {
|
||||||
|
fn id(&self) -> &dyn DeviceIdentifier {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn turn_off(&self) -> crate::error::Result<()> {
|
||||||
|
self.set_solid_color(
|
||||||
|
SolidColor::Custom {
|
||||||
|
red: 00,
|
||||||
|
green: 00,
|
||||||
|
blue: 00,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_solid_color(&self, color: SolidColor, blink: bool) -> crate::error::Result<()> {
|
||||||
|
info!("Setting the color of device '{}' to {}", self.id, color);
|
||||||
|
let (r, g, b) = match color {
|
||||||
|
SolidColor::Red => (255, 0, 0),
|
||||||
|
SolidColor::Green => (0, 255, 0),
|
||||||
|
SolidColor::Yellow => (255, 255, 0),
|
||||||
|
SolidColor::Blue => (0, 0, 255),
|
||||||
|
SolidColor::White => (255, 255, 255),
|
||||||
|
SolidColor::Cyan => (0, 255, 255),
|
||||||
|
SolidColor::Magenta => (255, 0, 255),
|
||||||
|
SolidColor::Custom { red, green, blue } => (red, green, blue),
|
||||||
|
};
|
||||||
|
let mode = if blink { MODE_STROBE } else { MODE_SOLID };
|
||||||
|
trace!("{} ({:#04x},{:#04x},{:#04x})", mode, r, g, b);
|
||||||
|
let result = self.hid_device.write(&[mode, LED_ALL, r, g, b]);
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
error!("Could not write to HID device: {:?}", err);
|
||||||
|
Err(crate::error::ErrorKind::InvalidRequest.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_pattern(&self, pattern: Pattern) -> crate::error::Result<()> {
|
||||||
|
info!("Setting the pattern of device '{}' to {}", self.id, pattern);
|
||||||
|
let pattern = match pattern {
|
||||||
|
Pattern::Police => PATTERN_POLICE,
|
||||||
|
Pattern::TrafficLights => PATTERN_LUXAFOR,
|
||||||
|
Pattern::Random(n) => match n {
|
||||||
|
1 => PATTERN_RANDOM_1,
|
||||||
|
2 => PATTERN_RANDOM_2,
|
||||||
|
3 => PATTERN_RANDOM_3,
|
||||||
|
4 => PATTERN_RANDOM_4,
|
||||||
|
_ => PATTERN_RANDOM_5,
|
||||||
|
},
|
||||||
|
Pattern::Rainbow => PATTERN_RAINBOW_WAVE,
|
||||||
|
Pattern::Sea => 9,
|
||||||
|
Pattern::WhiteWave => 10,
|
||||||
|
Pattern::Synthetic => 11,
|
||||||
|
};
|
||||||
|
let result = self.hid_device.write(&[MODE_PATTERN, LED_ALL, pattern]);
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
error!("Could not write to HID device: {:?}", err);
|
||||||
|
Err(crate::error::ErrorKind::InvalidRequest.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> USBDevice<'a> {
|
||||||
|
fn new(hid_device: HidDevice<'a>) -> crate::error::Result<USBDevice<'a>> {
|
||||||
|
let id = USBDeviceID(format!(
|
||||||
|
"{}::{}::{}",
|
||||||
|
hid_device
|
||||||
|
.get_manufacturer_string()
|
||||||
|
.unwrap_or("<unknown>".to_string()),
|
||||||
|
hid_device
|
||||||
|
.get_product_string()
|
||||||
|
.unwrap_or("<unknown>".to_string()),
|
||||||
|
hid_device
|
||||||
|
.get_serial_number_string()
|
||||||
|
.unwrap_or("<unknown>".to_string())
|
||||||
|
));
|
||||||
|
Ok(Self { hid_device, id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Unit Tests
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{Device, SolidColor};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_discovery() {
|
||||||
|
let result = super::USBDeviceDiscovery::new();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let discovery = result.unwrap();
|
||||||
|
|
||||||
|
let result = discovery.device();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let device = result.unwrap();
|
||||||
|
println!("{}", device.id());
|
||||||
|
|
||||||
|
let result = device.set_solid_color(SolidColor::Green, false);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
165
src/webhook.rs
Normal file
165
src/webhook.rs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
/*!
|
||||||
|
Implementation of the Device trait for webhook connected lights.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::{Device, DeviceIdentifier, Pattern, SolidColor};
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Public Types
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
///
|
||||||
|
/// The device identifier for a webhook connected light.
|
||||||
|
///
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct WebhookDeviceID(String);
|
||||||
|
|
||||||
|
///
|
||||||
|
/// The device implementation for a webhook connected light.
|
||||||
|
///
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct WebhookDevice {
|
||||||
|
id: WebhookDeviceID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Public Functions
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Return a device implementation for a webhook connected light.
|
||||||
|
///
|
||||||
|
pub fn new_device_for(device_id: &str) -> crate::error::Result<impl Device> {
|
||||||
|
let device_id = WebhookDeviceID::from_str(device_id)?;
|
||||||
|
Ok(WebhookDevice { id: device_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Private Types
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const API_V1: &str = "https://api.luxafor.com/webhook/v1/actions";
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Implementations
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
impl Display for WebhookDeviceID {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceIdentifier for WebhookDeviceID {}
|
||||||
|
|
||||||
|
impl FromStr for WebhookDeviceID {
|
||||||
|
type Err = crate::error::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if !s.is_empty() && s.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
|
Ok(Self(s.to_string()))
|
||||||
|
} else {
|
||||||
|
Err(crate::error::ErrorKind::InvalidDeviceID.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
impl Device for WebhookDevice {
|
||||||
|
fn id(&self) -> &dyn DeviceIdentifier {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn turn_off(&self) -> crate::error::Result<()> {
|
||||||
|
self.set_solid_color(
|
||||||
|
SolidColor::Custom {
|
||||||
|
red: 00,
|
||||||
|
green: 00,
|
||||||
|
blue: 00,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_solid_color(&self, color: SolidColor, blink: bool) -> crate::error::Result<()> {
|
||||||
|
info!("Setting the color of device '{}' to {}", self.id, color);
|
||||||
|
|
||||||
|
let body = if let SolidColor::Custom {
|
||||||
|
red: _,
|
||||||
|
green: _,
|
||||||
|
blue: _,
|
||||||
|
} = color
|
||||||
|
{
|
||||||
|
r#"{
|
||||||
|
"userId": "DID",
|
||||||
|
"actionFields":{
|
||||||
|
"color": "custom",
|
||||||
|
"custom_color": "COLOR"
|
||||||
|
}
|
||||||
|
}"#
|
||||||
|
.replace("DID", &self.id.to_string())
|
||||||
|
.replace("COLOR", &color.to_string())
|
||||||
|
} else {
|
||||||
|
r#"{
|
||||||
|
"userId": "DID",
|
||||||
|
"actionFields":{
|
||||||
|
"color": "COLOR"
|
||||||
|
}
|
||||||
|
}"#
|
||||||
|
.replace("DID", &self.id.to_string())
|
||||||
|
.replace("COLOR", &color.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = &format!("{}/{}", API_V1, if blink { "blink" } else { "solid_color" });
|
||||||
|
|
||||||
|
send_request(url, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_pattern(&self, pattern: Pattern) -> crate::error::Result<()> {
|
||||||
|
info!("Setting the pattern of device '{}' to {}", self.id, pattern);
|
||||||
|
|
||||||
|
let body = r#"{
|
||||||
|
"userId": "DID",
|
||||||
|
"actionFields":{
|
||||||
|
"pattern": "PATTERN"
|
||||||
|
}
|
||||||
|
}"#
|
||||||
|
.replace("DID", &self.id.to_string())
|
||||||
|
.replace("PATTERN", &pattern.to_string());
|
||||||
|
|
||||||
|
let url = &format!("{}/{}", API_V1, "pattern");
|
||||||
|
|
||||||
|
send_request(url, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Private Functions
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn send_request(api: &str, body: String) -> crate::error::Result<()> {
|
||||||
|
debug!("Sending to: {}", api);
|
||||||
|
debug!("Sending data: {:?}", body);
|
||||||
|
|
||||||
|
let client = Client::new();
|
||||||
|
let result = client
|
||||||
|
.post(api)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(body)
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
if result.status().is_success() {
|
||||||
|
info!("call successful");
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let status_code = result.status().as_u16();
|
||||||
|
error!("call failed");
|
||||||
|
error!("{:?}", result.text());
|
||||||
|
Err(crate::error::ErrorKind::UnexpectedError(status_code).into())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user