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]
|
||||
name = "luxafor"
|
||||
description = "Library, and CLI, for Luxafor lights via webhooks."
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Simon Johnston <johnstonskj@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
@ -14,7 +14,10 @@ targets = ["x86_64-unknown-linux-gnu"]
|
||||
all-features = true
|
||||
|
||||
[features]
|
||||
command-line = ["pretty_env_logger", "structopt"]
|
||||
default = ["webhook"]
|
||||
usb = ["hidapi"]
|
||||
webhook = ["reqwest"]
|
||||
command-line = ["pretty_env_logger", "structopt", "usb", "webhook"]
|
||||
|
||||
[[bin]]
|
||||
name = "lux"
|
||||
@ -24,8 +27,9 @@ required-features = ["command-line"]
|
||||
[dependencies]
|
||||
log = "0.4.11"
|
||||
error-chain = "0.12.2"
|
||||
reqwest = { version = "0.10", features = ["blocking", "json"] }
|
||||
|
||||
#[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 }
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
17
README.md
17
README.md
@ -1,6 +1,6 @@
|
||||
# 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/)
|
||||
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.
|
||||
|
||||
@ -36,9 +36,20 @@ The following shows the command line tool turning the light off.
|
||||
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
|
||||
|
||||
**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**
|
||||
|
||||
* Initial commit, supports flag and bluetooth lights.
|
||||
@ -46,4 +57,4 @@ The following shows the command line tool turning the light off.
|
||||
|
||||
## 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]
|
||||
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 structopt::StructOpt;
|
||||
|
||||
@ -14,7 +16,7 @@ pub(crate) struct CommandLine {
|
||||
|
||||
/// The device identifier
|
||||
#[structopt(long, short, env = "LUX_DEVICE")]
|
||||
device: DeviceID,
|
||||
device: String,
|
||||
|
||||
#[structopt(subcommand)]
|
||||
cmd: SubCommand,
|
||||
@ -37,7 +39,6 @@ pub(crate) enum SubCommand {
|
||||
/// Set the light to a to a pre-defined pattern
|
||||
Pattern {
|
||||
/// The pattern to set
|
||||
#[structopt(long, short)]
|
||||
pattern: Pattern,
|
||||
},
|
||||
/// Turn the light off
|
||||
@ -58,11 +59,23 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
})
|
||||
.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 {
|
||||
SubCommand::Solid { color } => set_solid_color(args.device, color, false),
|
||||
SubCommand::Blink { color } => set_solid_color(args.device, color, true),
|
||||
SubCommand::Pattern { pattern } => set_pattern(args.device, pattern),
|
||||
SubCommand::Off => turn_off(args.device),
|
||||
SubCommand::Solid { color } => device.set_solid_color(color, false),
|
||||
SubCommand::Blink { color } => device.set_solid_color(color, true),
|
||||
SubCommand::Pattern { pattern } => device.set_pattern(pattern),
|
||||
SubCommand::Off => device.turn_off(),
|
||||
}?;
|
||||
|
||||
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/)
|
||||
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
|
||||
```
|
||||
|
||||
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(
|
||||
@ -38,7 +52,7 @@ The following shows the command line tool turning the light off.
|
||||
trivial_numeric_casts,
|
||||
// ---------- Public
|
||||
missing_debug_implementations,
|
||||
//missing_docs,
|
||||
missing_docs,
|
||||
unreachable_pub,
|
||||
// ---------- Unsafe
|
||||
unsafe_code,
|
||||
@ -52,10 +66,10 @@ The following shows the command line tool turning the light off.
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use reqwest::blocking::Client;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
@ -63,12 +77,6 @@ use std::str::FromStr;
|
||||
// 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.
|
||||
///
|
||||
@ -89,7 +97,14 @@ pub enum SolidColor {
|
||||
/// A preset color
|
||||
Magenta,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Private Types
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
const API_V1: &str = "https://api.luxafor.com/webhook/v1/actions";
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Public Functions
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
///
|
||||
/// Trait describing a device identifier, basically you just need to be able to `to_string()` it.
|
||||
///
|
||||
pub trait DeviceIdentifier: Display {}
|
||||
|
||||
///
|
||||
/// Turn the light off.
|
||||
/// A trait implemented by different access methods to control a light.
|
||||
///
|
||||
pub fn turn_off(device: DeviceID) -> error::Result<()> {
|
||||
set_solid_color(
|
||||
device,
|
||||
SolidColor::Custom {
|
||||
red: 00,
|
||||
green: 00,
|
||||
blue: 00,
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
pub trait Device {
|
||||
///
|
||||
/// Return the identifier for the device.
|
||||
///
|
||||
fn id(&self) -> &dyn DeviceIdentifier;
|
||||
|
||||
///
|
||||
/// Set the color, and blink status, of the light.
|
||||
///
|
||||
pub fn set_solid_color(device: DeviceID, color: SolidColor, blink: bool) -> error::Result<()> {
|
||||
info!("Setting the color of device '{}' to {}", device, color);
|
||||
///
|
||||
/// Turn the light off.
|
||||
///
|
||||
fn turn_off(&self) -> error::Result<()>;
|
||||
|
||||
let body = if let SolidColor::Custom {
|
||||
red: _,
|
||||
green: _,
|
||||
blue: _,
|
||||
} = 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())
|
||||
};
|
||||
///
|
||||
/// Set the color, and blink status, of the light.
|
||||
///
|
||||
fn set_solid_color(&self, color: SolidColor, blink: bool) -> error::Result<()>;
|
||||
|
||||
let url = &format!("{}/{}", API_V1, if blink { "blink" } else { "solid_color" });
|
||||
|
||||
send_request(url, body)
|
||||
}
|
||||
|
||||
///
|
||||
/// 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)
|
||||
///
|
||||
/// Set the pattern displayed by the light.
|
||||
///
|
||||
fn set_pattern(&self, pattern: Pattern) -> error::Result<()>;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// 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 {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
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
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
mod error {
|
||||
///
|
||||
/// Error handling types.
|
||||
///
|
||||
#[allow(missing_docs)]
|
||||
pub mod error {
|
||||
error_chain! {
|
||||
errors {
|
||||
#[doc("The color value supplied was not recognized")]
|
||||
@ -358,6 +279,11 @@ mod error {
|
||||
description("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")]
|
||||
InvalidRequest {
|
||||
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