feat: early stages of a TP-Link Kasa driver for our smart lights
This commit is contained in:
17
driver/kasa/Cargo.toml
Normal file
17
driver/kasa/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "driver-kasa"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
backoff = { workspace = true, features = ["tokio"] }
|
||||||
|
deranged = { workspace = true }
|
||||||
|
mac_address = { version = "1.1.8", features = ["serde"] }
|
||||||
|
protocol = { path = "../../protocol" }
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
serde_repr = "0.1.20"
|
||||||
|
serde_with = "3.12.0"
|
||||||
|
snafu = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["io-util", "net", "sync", "time"] }
|
||||||
|
tracing = { workspace = true }
|
280
driver/kasa/src/connection.rs
Normal file
280
driver/kasa/src/connection.rs
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
use std::{convert::Infallible, io, net::SocketAddr, num::NonZero, time::Duration};
|
||||||
|
|
||||||
|
use backoff::{ExponentialBackoff, ExponentialBackoffBuilder};
|
||||||
|
use protocol::light::{Kelvin, KelvinLight, Light, Rgb, RgbLight};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
|
||||||
|
net::{TcpListener, TcpSocket, TcpStream},
|
||||||
|
sync::{mpsc, oneshot, OnceCell},
|
||||||
|
time::timeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::messages::{GetSysInfo, GetSysInfoResponse, LB130USSys, SysInfo};
|
||||||
|
|
||||||
|
struct XorEncryption<const INITIAL_KEY: u8>;
|
||||||
|
|
||||||
|
impl<const INITIAL_KEY: u8> XorEncryption<INITIAL_KEY> {
|
||||||
|
fn encrypt_in_place(bytes: &mut [u8]) {
|
||||||
|
let mut key = INITIAL_KEY;
|
||||||
|
for unencrypted_byte in bytes {
|
||||||
|
let encrypted_byte = key ^ *unencrypted_byte;
|
||||||
|
key = encrypted_byte;
|
||||||
|
*unencrypted_byte = encrypted_byte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_in_place(bytes: &mut [u8]) {
|
||||||
|
let mut key = INITIAL_KEY;
|
||||||
|
for encrypted_byte in bytes {
|
||||||
|
let unencrypted_byte = key ^ *encrypted_byte;
|
||||||
|
key = *encrypted_byte;
|
||||||
|
*encrypted_byte = unencrypted_byte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_encrypted(mut msg: Vec<u8>) -> Vec<u8> {
|
||||||
|
let length = msg.len() as u32;
|
||||||
|
let big_endian = length.to_be_bytes();
|
||||||
|
XorEncryption::<171>::encrypt_in_place(&mut msg);
|
||||||
|
|
||||||
|
let all_together = big_endian.into_iter().chain(msg);
|
||||||
|
|
||||||
|
all_together.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum CommunicationError {
|
||||||
|
SerializeError { source: serde_json::Error },
|
||||||
|
WriteError { source: std::io::Error },
|
||||||
|
ReadError { source: std::io::Error },
|
||||||
|
DeserializeError { source: serde_json::Error },
|
||||||
|
WrongDevice,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum LB130USMessage {
|
||||||
|
GetSysInfo(oneshot::Sender<Result<LB130USSys, CommunicationError>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn lb130us_actor(
|
||||||
|
addr: SocketAddr,
|
||||||
|
disconnect_after_idle: Duration,
|
||||||
|
mut messages: mpsc::Receiver<LB130USMessage>,
|
||||||
|
) {
|
||||||
|
let mut connection_cell = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (connection, message) = match &mut connection_cell {
|
||||||
|
Some(connection) => match timeout(disconnect_after_idle, messages.recv()).await {
|
||||||
|
Ok(Some(message)) => (connection, message),
|
||||||
|
Ok(None) => return,
|
||||||
|
Err(timed_out) => {
|
||||||
|
tracing::warn!(
|
||||||
|
?addr,
|
||||||
|
?timed_out,
|
||||||
|
"disconnecting from the LB130(US) because the idle timeout has been reached",
|
||||||
|
);
|
||||||
|
|
||||||
|
connection_cell.take();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let Some(message) = messages.recv().await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"connecting for a first time / reconnecting after having gone idle..."
|
||||||
|
);
|
||||||
|
|
||||||
|
match backoff::future::retry_notify(
|
||||||
|
ExponentialBackoff::default(),
|
||||||
|
|| async {
|
||||||
|
let stream = TcpStream::connect(addr).await?;
|
||||||
|
let (reader, writer) = stream.into_split();
|
||||||
|
|
||||||
|
let buf_reader = BufReader::new(reader);
|
||||||
|
let buf_writer = BufWriter::new(writer);
|
||||||
|
|
||||||
|
Ok((buf_reader, buf_writer))
|
||||||
|
},
|
||||||
|
|err, duration| {
|
||||||
|
tracing::error!(?err, ?duration);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(connection) => (connection_cell.insert(connection), message),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(?addr, ?err, "error connecting to an LB130(US)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (reader, writer) = connection;
|
||||||
|
|
||||||
|
tracing::info!("yay connected and got a message");
|
||||||
|
|
||||||
|
// TODO: do something
|
||||||
|
match message {
|
||||||
|
LB130USMessage::GetSysInfo(callback) => {
|
||||||
|
tracing::info!("going to try to get sys info for you...");
|
||||||
|
|
||||||
|
// TODO: extract to its own function
|
||||||
|
let outgoing = GetSysInfo;
|
||||||
|
let outgoing = match serde_json::to_vec(&outgoing) {
|
||||||
|
Ok(outgoing) => outgoing,
|
||||||
|
Err(err) => {
|
||||||
|
// TODO (continued) instead of doing stuff like this
|
||||||
|
let _ =
|
||||||
|
callback.send(Err(CommunicationError::SerializeError { source: err }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!(?outgoing);
|
||||||
|
|
||||||
|
let encrypted_outgoing = into_encrypted(outgoing);
|
||||||
|
|
||||||
|
tracing::info!(?encrypted_outgoing);
|
||||||
|
|
||||||
|
if let Err(err) = writer.write_all(&encrypted_outgoing).await {
|
||||||
|
connection_cell.take();
|
||||||
|
let _ = callback.send(Err(CommunicationError::WriteError { source: err }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = writer.flush().await {
|
||||||
|
connection_cell.take();
|
||||||
|
let _ = callback.send(Err(CommunicationError::WriteError { source: err }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tracing::info!("sent it, now about to try to get a response");
|
||||||
|
|
||||||
|
let incoming_length = match reader.read_u32().await {
|
||||||
|
Ok(incoming_length) => incoming_length,
|
||||||
|
Err(err) => {
|
||||||
|
connection_cell.take();
|
||||||
|
let _ = callback.send(Err(CommunicationError::ReadError { source: err }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tracing::info!(?incoming_length);
|
||||||
|
|
||||||
|
let mut incoming_message = Vec::new();
|
||||||
|
incoming_message.resize(incoming_length as usize, 0);
|
||||||
|
if let Err(err) = reader.read_exact(&mut incoming_message).await {
|
||||||
|
connection_cell.take();
|
||||||
|
let _ = callback.send(Err(CommunicationError::ReadError { source: err }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
XorEncryption::<171>::decrypt_in_place(&mut incoming_message);
|
||||||
|
tracing::info!(?incoming_message);
|
||||||
|
|
||||||
|
let response: GetSysInfoResponse = match serde_json::from_slice(&incoming_message) {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(err) => {
|
||||||
|
let _ = callback
|
||||||
|
.send(Err(CommunicationError::DeserializeError { source: err }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tracing::info!(?response);
|
||||||
|
|
||||||
|
let SysInfo::LB130US(lb130us) = response.system.get_sysinfo else {
|
||||||
|
let _ = callback.send(Err(CommunicationError::WrongDevice));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
tracing::info!(?lb130us);
|
||||||
|
|
||||||
|
let _ = callback.send(Ok(lb130us));
|
||||||
|
tracing::info!("cool, gave a response! onto the next message!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LB130USHandle {
|
||||||
|
sender: mpsc::Sender<LB130USMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum HandleError {
|
||||||
|
CommunicationError { source: CommunicationError },
|
||||||
|
Dead,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LB130USHandle {
|
||||||
|
pub fn new(addr: SocketAddr, disconnect_after_idle: Duration, buffer: NonZero<usize>) -> Self {
|
||||||
|
let (sender, receiver) = mpsc::channel(buffer.get());
|
||||||
|
tokio::spawn(lb130us_actor(addr, disconnect_after_idle, receiver));
|
||||||
|
Self { sender }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_sysinfo(&self) -> Result<LB130USSys, HandleError> {
|
||||||
|
let (sender, receiver) = oneshot::channel();
|
||||||
|
self.sender
|
||||||
|
.send(LB130USMessage::GetSysInfo(sender))
|
||||||
|
.await
|
||||||
|
.map_err(|_| HandleError::Dead)?;
|
||||||
|
receiver
|
||||||
|
.await
|
||||||
|
.map_err(|_| HandleError::Dead)?
|
||||||
|
.context(CommunicationSnafu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Light for LB130USHandle {
|
||||||
|
type IsOnError = Infallible; // TODO
|
||||||
|
|
||||||
|
async fn is_on(&self) -> Result<bool, Self::IsOnError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
type IsOffError = Infallible; // TODO
|
||||||
|
|
||||||
|
async fn is_off(&self) -> Result<bool, Self::IsOffError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
type TurnOnError = Infallible; // TODO
|
||||||
|
|
||||||
|
async fn turn_on(&mut self) -> Result<(), Self::TurnOnError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
type TurnOffError = Infallible; // TODO
|
||||||
|
|
||||||
|
async fn turn_off(&mut self) -> Result<(), Self::TurnOffError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToggleError = Infallible; // TODO
|
||||||
|
|
||||||
|
async fn toggle(&mut self) -> Result<(), Self::ToggleError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KelvinLight for LB130USHandle {
|
||||||
|
type TurnToKelvinError = Infallible; // TODO
|
||||||
|
|
||||||
|
async fn turn_to_kelvin(&mut self, temperature: Kelvin) -> Result<(), Self::TurnToKelvinError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RgbLight for LB130USHandle {
|
||||||
|
type TurnToRgbError = Infallible; // TODO
|
||||||
|
|
||||||
|
async fn turn_to_rgb(&mut self, color: Rgb) -> Result<(), Self::TurnToRgbError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
2
driver/kasa/src/lib.rs
Normal file
2
driver/kasa/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod connection;
|
||||||
|
pub mod messages;
|
277
driver/kasa/src/messages.rs
Normal file
277
driver/kasa/src/messages.rs
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
use std::{collections::BTreeMap, fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use deranged::{RangedU16, RangedU8};
|
||||||
|
use mac_address::{MacAddress, MacParseError};
|
||||||
|
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
|
||||||
|
use serde_repr::Deserialize_repr;
|
||||||
|
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GetSysInfo;
|
||||||
|
|
||||||
|
impl Serialize for GetSysInfo {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let target = "system";
|
||||||
|
let cmd = "get_sysinfo";
|
||||||
|
let arg: Option<()> = None;
|
||||||
|
|
||||||
|
let mut top_level_map = serializer.serialize_map(Some(1))?;
|
||||||
|
top_level_map.serialize_entry(target, &BTreeMap::from([(cmd, arg)]))?;
|
||||||
|
top_level_map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct GetSysInfoResponse {
|
||||||
|
pub system: GetSysInfoResponseSystem,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct GetSysInfoResponseSystem {
|
||||||
|
pub get_sysinfo: SysInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CommonSysInfo {
|
||||||
|
active_mode: ActiveMode,
|
||||||
|
alias: String,
|
||||||
|
ctrl_protocols: CtrlProtocols,
|
||||||
|
description: String,
|
||||||
|
dev_state: DevState,
|
||||||
|
#[serde(rename = "deviceId")]
|
||||||
|
device_id: DeviceId,
|
||||||
|
disco_ver: String,
|
||||||
|
err_code: i32, // No idea
|
||||||
|
heapsize: u64, // No idea
|
||||||
|
#[serde(rename = "hwId")]
|
||||||
|
hw_id: HardwareId,
|
||||||
|
hw_ver: String,
|
||||||
|
is_color: IsColor,
|
||||||
|
is_dimmable: IsDimmable,
|
||||||
|
is_factory: bool,
|
||||||
|
is_variable_color_temp: IsVariableColorTemp,
|
||||||
|
light_state: LightState,
|
||||||
|
mic_mac: MacAddressWithoutSeparators,
|
||||||
|
mic_type: MicType,
|
||||||
|
// model: Model,
|
||||||
|
#[serde(rename = "oemId")]
|
||||||
|
oem_id: OemId,
|
||||||
|
preferred_state: Vec<PreferredStateChoice>,
|
||||||
|
rssi: i32,
|
||||||
|
sw_ver: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct LB130USSys {
|
||||||
|
#[serde(flatten)]
|
||||||
|
sys_info: CommonSysInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(tag = "model")]
|
||||||
|
pub enum SysInfo {
|
||||||
|
#[serde(rename = "LB130(US)")]
|
||||||
|
LB130US(LB130USSys),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct PreferredStateChoice {
|
||||||
|
#[serde(flatten)]
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SerializeDisplay, DeserializeFromStr)]
|
||||||
|
struct MacAddressWithoutSeparators(MacAddress);
|
||||||
|
|
||||||
|
impl FromStr for MacAddressWithoutSeparators {
|
||||||
|
type Err = MacParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let [a, b, c, d, e, f, g, h, i, j, k, l] = s
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| MacParseError::InvalidLength)?;
|
||||||
|
|
||||||
|
let bytes = [(a, b), (c, d), (e, f), (g, h), (i, j), (k, l)];
|
||||||
|
|
||||||
|
let mut digits = [0; 6];
|
||||||
|
|
||||||
|
for (i, (one, two)) in bytes.into_iter().enumerate() {
|
||||||
|
let slice = [one, two];
|
||||||
|
let as_string = std::str::from_utf8(&slice).map_err(|_| MacParseError::InvalidDigit)?;
|
||||||
|
let number =
|
||||||
|
u8::from_str_radix(as_string, 16).map_err(|_| MacParseError::InvalidDigit)?;
|
||||||
|
digits[i] = number;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(MacAddressWithoutSeparators(MacAddress::new(digits)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MacAddressWithoutSeparators {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
enum ActiveMode {
|
||||||
|
#[serde(rename = "none")]
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct CtrlProtocols {
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct DeviceId(pub String);
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
enum DevState {
|
||||||
|
#[serde(rename = "normal")]
|
||||||
|
Normal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct HardwareId(pub String);
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize_repr)]
|
||||||
|
#[repr(u8)]
|
||||||
|
enum IsColor {
|
||||||
|
NoColor = 0,
|
||||||
|
Color = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize_repr)]
|
||||||
|
#[repr(u8)]
|
||||||
|
enum IsDimmable {
|
||||||
|
NotDimmable = 0,
|
||||||
|
Dimmable = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize_repr)]
|
||||||
|
#[repr(u8)]
|
||||||
|
enum IsVariableColorTemp {
|
||||||
|
NoVariableColorTemp = 0,
|
||||||
|
VariableColorTemp = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Percentage = RangedU8<0, 100>;
|
||||||
|
type Angle = RangedU16<0, 360>;
|
||||||
|
type Kelvin = RangedU16<2500, 9000>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct MaybeKelvin(Option<Kelvin>);
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for MaybeKelvin {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
match u16::deserialize(deserializer)? {
|
||||||
|
0 => Ok(MaybeKelvin(None)),
|
||||||
|
value => {
|
||||||
|
let kelvin = Kelvin::try_from(value).map_err(|e| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"{value} is not in the range {}..{}",
|
||||||
|
Kelvin::MIN,
|
||||||
|
Kelvin::MAX
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(MaybeKelvin(Some(kelvin)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct RawColor {
|
||||||
|
brightness: Percentage,
|
||||||
|
color_temp: MaybeKelvin,
|
||||||
|
hue: Angle,
|
||||||
|
saturation: Percentage,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Hsb {
|
||||||
|
hue: Angle,
|
||||||
|
saturation: Percentage,
|
||||||
|
brightness: Percentage,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct KelvinWithBrightness {
|
||||||
|
kelvin: Kelvin,
|
||||||
|
brightness: Percentage,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Color {
|
||||||
|
HSB(Hsb),
|
||||||
|
KelvinWithBrightness(KelvinWithBrightness),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Color {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let raw_color = RawColor::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
let RawColor {
|
||||||
|
brightness,
|
||||||
|
color_temp,
|
||||||
|
hue,
|
||||||
|
saturation,
|
||||||
|
} = raw_color;
|
||||||
|
|
||||||
|
match color_temp.0 {
|
||||||
|
Some(kelvin) => Ok(Color::KelvinWithBrightness(KelvinWithBrightness {
|
||||||
|
kelvin,
|
||||||
|
brightness,
|
||||||
|
})),
|
||||||
|
None => Ok(Color::HSB(Hsb {
|
||||||
|
hue,
|
||||||
|
saturation,
|
||||||
|
brightness,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct LightState {
|
||||||
|
#[serde(flatten)]
|
||||||
|
color: Color,
|
||||||
|
mode: LightStateMode,
|
||||||
|
on_off: OnOrOff,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
enum LightStateMode {
|
||||||
|
#[serde(rename = "normal")]
|
||||||
|
Normal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize_repr)]
|
||||||
|
#[repr(u8)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
enum OnOrOff {
|
||||||
|
Off = 0,
|
||||||
|
On = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
enum MicType {
|
||||||
|
#[serde(rename = "IOT.SMARTBULB")]
|
||||||
|
IotSmartbulb,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct OemId(pub String);
|
Reference in New Issue
Block a user