Initial commit.

This commit is contained in:
Simon Johnston 2020-08-17 16:06:18 -07:00
commit ad978e8007
10 changed files with 622 additions and 0 deletions

22
.github/workflows/rust.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Rust
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
## ------------------------------------------------------------------------------------------------
# Files created by development tools
/.idea/
*.iml
**/*.rs~
**/*.md~
## ------------------------------------------------------------------------------------------------
# Files created by cargo (build, fmt, make, etc.)
/.cargo
/target
/docs
Cargo.lock
**/*.rs.bk
## ------------------------------------------------------------------------------------------------
# Files required for CI integration (Travis)
/ci

12
.idea/luxafor.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/luxafor.iml" filepath="$PROJECT_DIR$/.idea/luxafor.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

31
Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "luxafor"
description = "Library, and CLI, for Luxafor lights via webhooks."
version = "0.1.0"
authors = ["Simon Johnston <johnstonskj@gmail.com>"]
edition = "2018"
license = "MIT"
readme = "README.md"
publish = true
default-run = "lux"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
all-features = true
[features]
command-line = ["pretty_env_logger", "structopt"]
[[bin]]
name = "lux"
path = "src/bin/main.rs"
required-features = ["command-line"]
[dependencies]
log = "0.4.11"
error-chain = "0.12.2"
reqwest = { version = "0.10", features = ["blocking", "json"] }
#[feature-dependencies]
structopt = { version = "0.3.14", optional = true }
pretty_env_logger = { version = "0.4.0", optional = true }

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Simon Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

47
README.md Normal file
View File

@ -0,0 +1,47 @@
#Crate luxafor
Library, and CLI, for [Luxafor](https://luxafor.com/products/) lights via webhooks.
![Rust](https://github.com/johnstonskj/luxafor/workflows/Rust/badge.svg)
![Minimum Rust Version](https://img.shields.io/badge/Min%20Rust-1.40-green.svg)
[![crates.io](https://img.shields.io/crates/v/atelier_core.svg)](https://crates.io/crates/atelier_core)
[![docs.rs](https://docs.rs/atelier_core/badge.svg)](https://docs.rs/atelier_core)
![MIT License](https://img.shields.io/badge/license-mit-118811.svg)
[![GitHub stars](https://img.shields.io/github/stars/johnstonskj/luxafor.svg)](https://github.com/johnstonskj/luxafor/stargazers)
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
The following shows the command line tool setting the color to red.
```bash
lux solid red -d 2a0f2c73b72
```
The following shows the command line tool setting the color to a blinking green.
```bash
lux blink green -d 2a0f2c73b72
```
The following shows the command line tool turning the light off.
```bash
lux -vvv off -d 2a0f2c73b72
INFO luxafor > Setting the color of device '2a0f2c73b72e' to 000000
INFO luxafor > call successful
```
## Changes
**Version 0.1.0**
* Initial commit, supports flag and bluetooth lights.
## TODO
TBD

81
src/bin/main.rs Normal file
View File

@ -0,0 +1,81 @@
#[macro_use]
extern crate log;
use luxafor::{set_pattern, set_solid_color, turn_off, DeviceID, Pattern, SolidColor};
use std::error::Error;
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
#[structopt(name = "lux", about = "CLI for Luxafor lights")]
pub(crate) struct CommandLine {
/// The level of logging to perform; from off to trace
#[structopt(long, short = "v", parse(from_occurrences))]
verbose: i8,
#[structopt(subcommand)]
cmd: SubCommand,
}
#[derive(Debug, StructOpt)]
pub(crate) enum SubCommand {
/// Set the light to a to a solid color
Solid {
/// The device identifier
#[structopt(long, short)]
device: DeviceID,
/// The color to set
#[structopt(name = "COLOR")]
color: SolidColor,
},
/// Set the light to a to a blinking color
Blink {
/// The device identifier
#[structopt(long, short)]
device: DeviceID,
/// The color to set
#[structopt(name = "COLOR")]
color: SolidColor,
},
/// Set the light to a to a pre-defined pattern
Pattern {
/// The device identifier
#[structopt(long, short)]
device: DeviceID,
/// The pattern to set
#[structopt(long, short)]
pattern: Pattern,
},
/// Turn the light off
Off {
/// The device identifier
#[structopt(long, short)]
device: DeviceID,
},
}
fn main() -> Result<(), Box<dyn Error>> {
let args = CommandLine::from_args();
pretty_env_logger::formatted_builder()
.filter_level(match args.verbose {
0 => log::LevelFilter::Off,
1 => log::LevelFilter::Error,
2 => log::LevelFilter::Warn,
3 => log::LevelFilter::Info,
4 => log::LevelFilter::Debug,
_ => log::LevelFilter::Trace,
})
.init();
match args.cmd {
SubCommand::Solid { device, color } => set_solid_color(device, color, false),
SubCommand::Blink { device, color } => set_solid_color(device, color, true),
SubCommand::Pattern { device, pattern } => set_pattern(device, pattern),
SubCommand::Off { device } => turn_off(device),
}?;
Ok(())
}

376
src/lib.rs Normal file
View File

@ -0,0 +1,376 @@
/*!
Library, and CLI, for [Luxafor](https://luxafor.com/products/) lights via webhooks. 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
The following shows the command line tool setting the color to red.
```bash
lux solid red -d 2a0f2c73b72
```
The following shows the command line tool setting the color to a blinking green.
```bash
lux blink green -d 2a0f2c73b72
```
The following shows the command line tool turning the light off.
```bash
lux -vvv off -d 2a0f2c73b72
INFO luxafor > Setting the color of device '2a0f2c73b72e' to 000000
INFO luxafor > call successful
```
*/
#![warn(
// ---------- Stylistic
future_incompatible,
nonstandard_style,
rust_2018_idioms,
trivial_casts,
trivial_numeric_casts,
// ---------- Public
missing_debug_implementations,
//missing_docs,
unreachable_pub,
// ---------- Unsafe
unsafe_code,
// ---------- Unused
unused_extern_crates,
unused_import_braces,
unused_qualifications,
unused_results,
)]
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate log;
use reqwest::blocking::Client;
use std::fmt::{Display, Formatter};
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.
///
#[derive(Clone, Debug)]
pub enum SolidColor {
/// A preset color
Red,
/// A preset color
Green,
/// A preset color
Yellow,
/// A preset color
Blue,
/// A preset color
White,
/// A preset color
Cyan,
/// A preset color
Magenta,
/// A custom color using standard RGB values
Custom { red: u8, green: u8, blue: u8 },
}
///
/// A pattern the light can be set to show.
#[derive(Clone, Debug)]
pub enum Pattern {
/// A preset pattern
Police,
/// A preset pattern
TrafficLights,
/// A preset pattern
Random(u8),
/// A preset pattern (accepted on Windows only)
Rainbow,
/// A preset pattern (accepted on Windows only)
Sea,
/// A preset pattern (accepted on Windows only)
WhiteWave,
/// A preset pattern (accepted on Windows only)
Synthetic,
}
// ------------------------------------------------------------------------------------------------
// Private Types
// ------------------------------------------------------------------------------------------------
const API_V1: &str = "https://api.luxafor.com/webhook/v1/actions";
// ------------------------------------------------------------------------------------------------
// Public Functions
// ------------------------------------------------------------------------------------------------
///
/// Turn the light off.
///
pub fn turn_off(device: DeviceID) -> error::Result<()> {
set_solid_color(
device,
SolidColor::Custom {
red: 00,
green: 00,
blue: 00,
},
false,
)
}
///
/// 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);
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())
};
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)
}
// ------------------------------------------------------------------------------------------------
// 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 {
format!("{:#04x}", v)[2..].to_string()
}
write!(
f,
"{}",
match self {
SolidColor::Red => "red".to_string(),
SolidColor::Green => "green".to_string(),
SolidColor::Yellow => "yellow".to_string(),
SolidColor::Blue => "blue".to_string(),
SolidColor::White => "white".to_string(),
SolidColor::Cyan => "cyan".to_string(),
SolidColor::Magenta => "magenta".to_string(),
SolidColor::Custom { red, green, blue } =>
format!("{}{}{}", to_hex(red), to_hex(green), to_hex(blue)),
}
)
}
}
impl FromStr for SolidColor {
type Err = error::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase();
match s.as_str() {
"red" => Ok(SolidColor::Red),
"green" => Ok(SolidColor::Green),
"yellow" => Ok(SolidColor::Yellow),
"blue" => Ok(SolidColor::Blue),
"white" => Ok(SolidColor::White),
"cyan" => Ok(SolidColor::Cyan),
"magenta" => Ok(SolidColor::Magenta),
_ => {
if s.len() == 6 && s.chars().all(|c| c.is_ascii_hexdigit()) {
Ok(SolidColor::Custom {
red: u8::from_str_radix(&s[0..1], 16)?,
green: u8::from_str_radix(&s[2..3], 16)?,
blue: u8::from_str_radix(&s[4..5], 16)?,
})
} else {
Err(error::ErrorKind::InvalidColor.into())
}
}
}
}
}
// ------------------------------------------------------------------------------------------------
impl Display for Pattern {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Pattern::Police => "police".to_string(),
Pattern::TrafficLights => "traffic lights".to_string(),
Pattern::Random(n) => format!("random {}", n),
Pattern::Rainbow => "rainbow".to_string(),
Pattern::Sea => "sea".to_string(),
Pattern::WhiteWave => "white wave".to_string(),
Pattern::Synthetic => "synthetic".to_string(),
}
)
}
}
impl FromStr for Pattern {
type Err = error::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase();
match s.as_str() {
"police" => Ok(Pattern::Police),
"traffic lights" => Ok(Pattern::TrafficLights),
"random 1" => Ok(Pattern::Random(1)),
"random 2" => Ok(Pattern::Random(2)),
"random 3" => Ok(Pattern::Random(3)),
"random 4" => Ok(Pattern::Random(4)),
"random 5" => Ok(Pattern::Random(5)),
"sea" => Ok(Pattern::Sea),
"white wave" => Ok(Pattern::WhiteWave),
"synthetic" => Ok(Pattern::Synthetic),
_ => Err(error::ErrorKind::InvalidPattern.into()),
}
}
}
// ------------------------------------------------------------------------------------------------
// 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_chain! {
errors {
#[doc("The color value supplied was not recognized")]
InvalidColor {
description("The color value supplied was not recognized")
display("The color value supplied was not recognized")
}
#[doc("The pattern value supplied was not recognized")]
InvalidPattern {
description("The pattern value supplied was not recognized")
display("The pattern value supplied was not recognized")
}
#[doc("The provided device ID was incorrectly formatted")]
InvalidDeviceID {
description("The provided device ID was incorrectly formatted")
display("The provided device ID was incorrectly formatted")
}
#[doc("The server indicated an invalid request")]
InvalidRequest {
description("The server indicated an invalid request")
display("The server indicated an invalid request")
}
#[doc("An unexpected HTTP error was returned")]
UnexpectedError(sc: u16) {
description("An unexpected HTTP error was returned")
display("An unexpected HTTP error was returned: {}", sc)
}
}
foreign_links {
CustomFmt(::std::num::ParseIntError);
Request(::reqwest::Error);
Fmt(::std::fmt::Error);
}
}
}