From 90192b6a415fe743f593fa055aae9c44fdb9dff8 Mon Sep 17 00:00:00 2001 From: Simon Johnston Date: Wed, 19 Aug 2020 14:42:10 -0700 Subject: [PATCH] Added new methods to `Device`, and added new `TargetedDevice` trait --- Cargo.toml | 4 +- README.md | 9 +++ src/bin/main.rs | 66 +++++++++++++-- src/lib.rs | 159 +++++++++++++++++++++++++++++++++++-- src/usb_hid.rs | 207 ++++++++++++++++++++++++++++++++++++------------ src/webhook.rs | 89 ++++++++++++++++++--- 6 files changed, 457 insertions(+), 77 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e22ccb1..e0d8fb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "luxafor" description = "Library, and CLI, for Luxafor lights via either USB or webhooks." -version = "0.2.1" +version = "0.2.2" authors = ["Simon Johnston "] repository = "https://github.com/johnstonskj/rust-luxafor" documentation = "https://docs.rs/luxafor/0.1.0/luxafor/" @@ -31,7 +31,7 @@ log = "0.4.11" error-chain = "0.12.2" #[feature-dependencies] -hidapi = { version = "~0.4", optional = true } +hidapi = { version = "1.2.3", 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 } diff --git a/README.md b/README.md index 38182af..adbb058 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,15 @@ The following shows the command line tool turning the light off. ## Changes +**Version 02.2.** + +* Added HID specification documents to the `usb_hid` module. +* Added new `Wave` enum +* Added new methods to `Device`. + * Fully supported on USB HID, + * Not all are supported on webooks. +* Added new `TargetedDevice` trait and USB implementation. + **Version 0.2.1** * Removed the `DeviceIdentifier` trait, and `Device` now returns a String. diff --git a/src/bin/main.rs b/src/bin/main.rs index ab3b0e2..57e159d 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -3,7 +3,7 @@ extern crate log; use luxafor::usb_hid::USBDeviceDiscovery; -use luxafor::{webhook, Device, Pattern, SolidColor}; +use luxafor::{webhook, Device, Pattern, SolidColor, Wave}; use std::error::Error; use structopt::StructOpt; @@ -30,16 +30,56 @@ pub(crate) enum SubCommand { #[structopt(name = "COLOR")] color: SolidColor, }, - /// Set the light to a to a blinking color - Blink { + /// Set the light to a to a strobing/blinking color + Strobe { /// The color to set #[structopt(name = "COLOR")] color: SolidColor, + + /// The speed of each strobe cycle + #[structopt(long, short, default_value = "10")] + speed: u8, + + /// The number of times to repeat the strobe + #[structopt(long, short, default_value = "255")] + repeat: u8, + }, + /// Set the light to fade from the current to a new color + Fade { + /// The color to set + #[structopt(name = "COLOR")] + color: SolidColor, + + /// The speed of each strobe cycle + #[structopt(long, short, default_value = "60")] + fade_duration: u8, + }, + /// Set the light to a to a pre-defined wave pattern + Wave { + /// The color to set + #[structopt(name = "COLOR")] + color: SolidColor, + + /// The pattern to set + #[structopt(default_value = "short")] + pattern: Wave, + + /// The speed of each wave cycle + #[structopt(long, short, default_value = "30")] + speed: u8, + + /// The number of times to repeat the pattern + #[structopt(long, short, default_value = "255")] + repeat: u8, }, /// Set the light to a to a pre-defined pattern Pattern { /// The pattern to set pattern: Pattern, + + /// The number of times to repeat the pattern + #[structopt(long, short, default_value = "255")] + repeat: u8, }, /// Turn the light off Off, @@ -74,9 +114,23 @@ fn main() -> Result<(), Box> { fn set_lights(args: CommandLine, device: impl Device) -> Result<(), Box> { match args.cmd { - 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::Solid { color } => device.set_solid_color(color), + SubCommand::Fade { + color, + fade_duration, + } => device.set_fade_to_color(color, fade_duration), + SubCommand::Strobe { + color, + speed, + repeat, + } => device.set_color_strobe(color, speed, repeat), + SubCommand::Wave { + color, + pattern, + speed, + repeat, + } => device.set_color_wave(color, pattern, speed, repeat), + SubCommand::Pattern { pattern, repeat } => device.set_pattern(pattern, repeat), SubCommand::Off => device.turn_off(), }?; diff --git a/src/lib.rs b/src/lib.rs index 18b94be..8dd1e2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,18 +16,20 @@ the use of a USB connected device. ```rust,ignore use luxafor::usb_hid::USBDeviceDiscovery; -use luxafor::{Device, SolidColor}; +use luxafor::{Device, SolidColor, TargetedDevice}; use luxafor::error::Result; fn set_do_not_disturb() -> Result<()> { let discovery = USBDeviceDiscovery::new()?; let device = discovery.device()?; println!("USB device: '{}'", device.id()); + device.set_specific_led(SpecificLED::AllFront); device.set_solid_color(SolidColor::Red, false) } ``` -The following shows the same function but using the webhook connection. +The following shows the same function but using the webhook connection. Note that the webhook API +is more limited in the features it exposes; it does not support `set_specific_led` for a start. ```rust,ignore use luxafor::webhook::new_device_for; @@ -143,8 +145,25 @@ pub enum SolidColor { }, } +/// +/// Waves produce a pattern that starts at the bottom of the light, fills the light and then +/// fades out at the top. +/// +#[derive(Clone, Debug)] +pub enum Wave { + /// A short transition, completed before the next wave starts. + Short, + /// A long transition, completed before the next wave starts. + Long, + /// A short transition, which _does not_ complete before the next wave starts. + OverlappingShort, + /// A long transition, which _does not_ complete before the next wave starts. + OverlappingLong, +} + /// /// A pattern the light can be set to show. +/// #[derive(Clone, Debug)] pub enum Pattern { /// A preset pattern that cycles between red and blue. @@ -182,14 +201,63 @@ pub trait Device { fn turn_off(&self) -> error::Result<()>; /// - /// Set the color, and blink status, of the light. + /// Set the light to a continuous solid color. /// - fn set_solid_color(&self, color: SolidColor, blink: bool) -> error::Result<()>; + fn set_solid_color(&self, color: SolidColor) -> error::Result<()>; /// - /// Set the pattern displayed by the light. + /// Set the light to fade from its current color to a new one. /// - fn set_pattern(&self, pattern: Pattern) -> error::Result<()>; + fn set_fade_to_color(&self, color: SolidColor, fade_duration: u8) -> error::Result<()>; + + /// + /// Strobe the light, this will dim and brighten the same color. + /// + fn set_color_strobe( + &self, + color: SolidColor, + strobe_speed: u8, + repeat_count: u8, + ) -> error::Result<()>; + + /// + /// Set the light to repeat one of a pre-defined set of wave patterns. + /// + fn set_color_wave( + &self, + color: SolidColor, + wave_pattern: Wave, + wave_speed: u8, + repeat_count: u8, + ) -> error::Result<()>; + + /// + /// Set the light to repeat one of a pre-defined set of patterns. + /// + fn set_pattern(&self, pattern: Pattern, repeat_count: u8) -> error::Result<()>; +} + +/// +/// Denotes which LED in the light should be the target of any device operations. +/// +#[derive(Clone, Debug)] +pub enum SpecificLED { + /// All supported LEDs + All, + /// Only the LEDs on the front (tab) of the light + AllFront, + /// Only the LEDs on the back of the light + AllBack, + /// Only one specific LED (value: 1..6) + Number(u8), +} + +/// +/// Extension trait to allow targeting specific LEDs on the device. +/// +pub trait TargetedDevice: Device { + /// Set the LED to be used for future operations. + fn set_specific_led(&mut self, led: SpecificLED) -> error::Result<()>; } // ------------------------------------------------------------------------------------------------ @@ -249,6 +317,38 @@ impl FromStr for SolidColor { // ------------------------------------------------------------------------------------------------ +impl Display for Wave { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Wave::Short => "short", + Wave::Long => "long", + Wave::OverlappingShort => "overlapping short", + Wave::OverlappingLong => "overlapping long", + } + ) + } +} + +impl FromStr for Wave { + type Err = error::Error; + + fn from_str(s: &str) -> Result { + let s = s.to_lowercase(); + match s.as_str() { + "short" => Ok(Wave::Short), + "long" => Ok(Wave::Long), + "overlapping short" => Ok(Wave::OverlappingShort), + "overlapping long" => Ok(Wave::OverlappingLong), + _ => Err(error::ErrorKind::InvalidPattern.into()), + } + } +} + +// ------------------------------------------------------------------------------------------------ + impl Display for Pattern { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( @@ -297,6 +397,43 @@ impl FromStr for Pattern { } } +// ------------------------------------------------------------------------------------------------ + +impl Display for SpecificLED { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + SpecificLED::All => "all".to_string(), + SpecificLED::AllFront => "front".to_string(), + SpecificLED::AllBack => "back".to_string(), + SpecificLED::Number(n) => n.to_string(), + } + ) + } +} + +impl FromStr for SpecificLED { + type Err = error::Error; + + fn from_str(s: &str) -> Result { + let s = s.to_lowercase(); + match s.as_str() { + "all" => Ok(SpecificLED::All), + "front" => Ok(SpecificLED::AllFront), + "back" => Ok(SpecificLED::AllBack), + "1" => Ok(SpecificLED::Number(1)), + "2" => Ok(SpecificLED::Number(2)), + "3" => Ok(SpecificLED::Number(3)), + "4" => Ok(SpecificLED::Number(4)), + "5" => Ok(SpecificLED::Number(5)), + "6" => Ok(SpecificLED::Number(6)), + _ => Err(error::ErrorKind::InvalidLED.into()), + } + } +} + // ------------------------------------------------------------------------------------------------ // Modules // ------------------------------------------------------------------------------------------------ @@ -318,6 +455,11 @@ pub mod error { description("The pattern value supplied was not recognized") display("The pattern value supplied was not recognized") } + #[doc("The LED number is either invalid or not supported by the connected device")] + InvalidLED { + description("The LED number is either invalid or not supported by the connected device") + display("The LED number is either invalid or not supported by the connected device") + } #[doc("The provided device ID was incorrectly formatted")] InvalidDeviceID { description("The provided device ID was incorrectly formatted") @@ -338,6 +480,11 @@ pub mod error { description("An unexpected HTTP error was returned") display("An unexpected HTTP error was returned: {}", sc) } + #[doc("The command is not supported by the current device, or connection to the device")] + UnsupportedCommand { + description("The command is not supported by the current device, or connection to the device") + display("The command is not supported by the current device, or connection to the device") + } } foreign_links { CustomFmt(::std::num::ParseIntError); diff --git a/src/usb_hid.rs b/src/usb_hid.rs index 1a10b3f..5314fcd 100644 --- a/src/usb_hid.rs +++ b/src/usb_hid.rs @@ -104,7 +104,7 @@ The serial number is returned as a pair, (high,low) bytes. */ -use crate::{Device, Pattern, SolidColor}; +use crate::{Device, Pattern, SolidColor, SpecificLED, TargetedDevice, Wave}; use hidapi::{HidApi, HidDevice}; // ------------------------------------------------------------------------------------------------ @@ -123,9 +123,10 @@ pub struct USBDeviceDiscovery { /// The device implementation for a USB connected light. /// #[allow(missing_debug_implementations)] -pub struct USBDevice<'a> { - hid_device: HidDevice<'a>, +pub struct USBDevice { + hid_device: HidDevice, id: String, + target_led: u8, } // ------------------------------------------------------------------------------------------------ @@ -137,32 +138,30 @@ const LUXAFOR_PRODUCT_ID: u16 = 0xf372; const HID_REPORT_ID: u8 = 0; +const MODE_SIMPLE: u8 = 0; 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 SIMPLE_COLOR_OFF: u8 = b'O'; + 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 WAVE_SHORT: u8 = 1; +const WAVE_LONG: u8 = 2; +const WAVE_OVERLAPPING_SHORT: u8 = 3; +const WAVE_OVERLAPPING_LONG: u8 = 4; + const PATTERN_LUXAFOR: u8 = 1; const PATTERN_RANDOM_1: u8 = 2; const PATTERN_RANDOM_2: u8 = 3; @@ -186,14 +185,19 @@ impl USBDeviceDiscovery { /// Construct a new discovery object, this initializes the USB HID interface and thus can fail. /// pub fn new() -> crate::error::Result { - let hid_api = HidApi::new()?; - Ok(Self { hid_api }) + match HidApi::new() { + Ok(hid_api) => Ok(Self { hid_api }), + Err(err) => { + error!("Could not connect to USB, error: {:?}", err); + Err(crate::error::ErrorKind::DeviceNotFound.into()) + } + } } /// /// Return a device, if found, that corresponds to a Luxafor light. /// - pub fn device(&self) -> crate::error::Result> { + pub fn device(&self) -> crate::error::Result { let result = self.hid_api.open(LUXAFOR_VENDOR_ID, LUXAFOR_PRODUCT_ID); match result { Ok(hid_device) => USBDevice::new(hid_device), @@ -207,40 +211,95 @@ impl USBDeviceDiscovery { // ------------------------------------------------------------------------------------------------ -impl<'a> Device for USBDevice<'a> { +impl Device for USBDevice { fn id(&self) -> String { self.id.clone() } fn turn_off(&self) -> crate::error::Result<()> { - self.set_solid_color( - SolidColor::Custom { - red: 00, - green: 00, - blue: 00, - }, - false, - ) + info!("Turning device '{}' off", self.id); + self.write(&[HID_REPORT_ID, MODE_SIMPLE, SIMPLE_COLOR_OFF]) } - fn set_solid_color(&self, color: SolidColor, blink: bool) -> crate::error::Result<()> { + fn set_solid_color(&self, color: SolidColor) -> 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); - self.write(&[HID_REPORT_ID, mode, LED_ALL, r, g, b]) + let (r, g, b) = self.color_to_bytes(color); + self.write(&[HID_REPORT_ID, MODE_SOLID, self.target_led, r, g, b]) } - fn set_pattern(&self, pattern: Pattern) -> crate::error::Result<()> { + fn set_fade_to_color(&self, color: SolidColor, fade_duration: u8) -> crate::error::Result<()> { + info!( + "Setting the fade-to color of device '{}' to {}, over {}", + self.id, color, fade_duration + ); + let (r, g, b) = self.color_to_bytes(color); + self.write(&[ + HID_REPORT_ID, + MODE_FADE, + self.target_led, + r, + g, + b, + fade_duration, + ]) + } + + fn set_color_strobe( + &self, + color: SolidColor, + strobe_speed: u8, + repeat_count: u8, + ) -> crate::error::Result<()> { + info!( + "Setting the device '{}' to strobe {}, at {}, {} times", + self.id, color, strobe_speed, repeat_count + ); + let (r, g, b) = self.color_to_bytes(color); + self.write(&[ + HID_REPORT_ID, + MODE_STROBE, + self.target_led, + r, + g, + b, + strobe_speed, + 0x00, + repeat_count, + ]) + } + + fn set_color_wave( + &self, + color: SolidColor, + wave_pattern: Wave, + wave_speed: u8, + repeat_count: u8, + ) -> crate::error::Result<()> { + info!( + "Setting the device '{}' to wave {}, at {}, {} times", + self.id, color, wave_speed, repeat_count + ); + let wave_pattern = match wave_pattern { + Wave::Short => WAVE_SHORT, + Wave::Long => WAVE_LONG, + Wave::OverlappingShort => WAVE_OVERLAPPING_SHORT, + Wave::OverlappingLong => WAVE_OVERLAPPING_LONG, + }; + let (r, g, b) = self.color_to_bytes(color); + self.write(&[ + HID_REPORT_ID, + MODE_WAVE, + wave_pattern, + r, + g, + b, + 0x00, + repeat_count, + wave_speed, + ]) + } + + fn set_pattern(&self, pattern: Pattern, repeat_count: u8) -> crate::error::Result<()> { info!("Setting the pattern of device '{}' to {}", self.id, pattern); let pattern = match pattern { Pattern::Police => PATTERN_POLICE, @@ -261,25 +320,65 @@ impl<'a> Device for USBDevice<'a> { #[cfg(target_os = "windows")] Pattern::Synthetic => 11, }; - self.write(&[HID_REPORT_ID, MODE_PATTERN, pattern, 255]) + self.write(&[HID_REPORT_ID, MODE_PATTERN, pattern, repeat_count]) } } -impl<'a> USBDevice<'a> { - fn new(hid_device: HidDevice<'a>) -> crate::error::Result> { +impl TargetedDevice for USBDevice { + fn set_specific_led(&mut self, led: SpecificLED) -> crate::error::Result<()> { + self.target_led = match led { + SpecificLED::All => LED_ALL, + SpecificLED::AllFront => LED_FRONT_ALL, + SpecificLED::AllBack => LED_BACK_ALL, + SpecificLED::Number(n) => match n { + 1 => LED_FRONT_BOTTOM, + 2 => LED_FRONT_MIDDLE, + 3 => LED_FRONT_TOP, + 4 => LED_BACK_BOTTOM, + 5 => LED_BACK_MIDDLE, + 6 => LED_BACK_TOP, + _ => return Err(crate::error::ErrorKind::InvalidLED.into()), + }, + }; + Ok(()) + } +} + +impl USBDevice { + fn new(hid_device: HidDevice) -> crate::error::Result { let id = format!( "{}::{}::{}", hid_device .get_manufacturer_string() + .unwrap_or(Some("".to_string())) .unwrap_or("".to_string()), hid_device .get_product_string() + .unwrap_or(Some("".to_string())) .unwrap_or("".to_string()), hid_device .get_serial_number_string() - .unwrap_or("".to_string()) + .unwrap_or(Some("".to_string())) + .unwrap_or("".to_string()), ); - Ok(Self { hid_device, id }) + Ok(Self { + hid_device, + id, + target_led: LED_ALL, + }) + } + + fn color_to_bytes(&self, color: SolidColor) -> (u8, u8, u8) { + 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), + } } fn write(&self, buffer: &[u8]) -> crate::error::Result<()> { @@ -293,11 +392,17 @@ impl<'a> USBDevice<'a> { ); let result = self.hid_device.write(buffer); match result { - Ok(bytes_written) => if bytes_written == buffer.len() { - Ok(()) - } else { - error!("Bytes written, {}, did not match buffer length {}", bytes_written, buffer.len()); - Err(crate::error::ErrorKind::InvalidRequest.into()) + Ok(bytes_written) => { + if bytes_written == buffer.len() { + Ok(()) + } else { + error!( + "Bytes written, {}, did not match buffer length {}", + bytes_written, + buffer.len() + ); + Err(crate::error::ErrorKind::InvalidRequest.into()) + } } Err(err) => { error!("Could not write to HID device: {:?}", err); @@ -326,7 +431,7 @@ mod tests { let device = result.unwrap(); println!("{}", device.id()); - let result = device.set_solid_color(SolidColor::Green, false); + let result = device.set_solid_color(SolidColor::Green); assert!(result.is_ok()); } } diff --git a/src/webhook.rs b/src/webhook.rs index 2ede868..ee1037b 100644 --- a/src/webhook.rs +++ b/src/webhook.rs @@ -3,7 +3,7 @@ Implementation of the Device trait for webhook connected lights. */ -use crate::{Device, Pattern, SolidColor}; +use crate::{Device, Pattern, SolidColor, Wave}; use reqwest::blocking::Client; // ------------------------------------------------------------------------------------------------ @@ -51,17 +51,14 @@ impl Device for WebhookDevice { } fn turn_off(&self) -> crate::error::Result<()> { - self.set_solid_color( - SolidColor::Custom { - red: 00, - green: 00, - blue: 00, - }, - false, - ) + self.set_solid_color(SolidColor::Custom { + red: 00, + green: 00, + blue: 00, + }) } - fn set_solid_color(&self, color: SolidColor, blink: bool) -> crate::error::Result<()> { + fn set_solid_color(&self, color: SolidColor) -> crate::error::Result<()> { info!("Setting the color of device '{}' to {}", self.id, color); let body = if let SolidColor::Custom { @@ -90,13 +87,81 @@ impl Device for WebhookDevice { .replace("COLOR", &color.to_string()) }; - let url = &format!("{}/{}", API_V1, if blink { "blink" } else { "solid_color" }); + let url = &format!("{}/{}", API_V1, "solid_color"); send_request(url, body) } - fn set_pattern(&self, pattern: Pattern) -> crate::error::Result<()> { + fn set_fade_to_color( + &self, + _color: SolidColor, + _fade_duration: u8, + ) -> crate::error::Result<()> { + Err(crate::error::ErrorKind::UnsupportedCommand.into()) + } + + fn set_color_strobe( + &self, + color: SolidColor, + _strobe_speed: u8, + repeat_count: u8, + ) -> crate::error::Result<()> { + info!( + "Setting the strobe color of device '{}' to {}", + self.id, color + ); + + let body = if let SolidColor::Custom { + red: _, + green: _, + blue: _, + } = color + { + r#"{ + "userId": "DID", + "actionFields":{ + "repeat": RPT, + "color": "custom", + "custom_color": "COLOR" + } +}"# + .replace("DID", &self.id.to_string()) + .replace("COLOR", &color.to_string()) + .replace("RPT", &repeat_count.to_string()) + } else { + r#"{ + "userId": "DID", + "actionFields":{ + "repeat": RPT, + "color": "COLOR" + } +}"# + .replace("DID", &self.id.to_string()) + .replace("COLOR", &color.to_string()) + .replace("RPT", &repeat_count.to_string()) + }; + + let url = &format!("{}/{}", API_V1, "blink"); + + send_request(url, body) + } + + fn set_color_wave( + &self, + _color: SolidColor, + _wave_pattern: Wave, + _wave_speed: u8, + _repeat_count: u8, + ) -> crate::error::Result<()> { + Err(crate::error::ErrorKind::UnsupportedCommand.into()) + } + + fn set_pattern(&self, pattern: Pattern, repeat_count: u8) -> crate::error::Result<()> { info!("Setting the pattern of device '{}' to {}", self.id, pattern); + warn!( + "Ignoring repeat count {}, not supported in the webhook API", + repeat_count + ); let body = r#"{ "userId": "DID",