Connect lifx and mqtt

This commit is contained in:
Lara 2019-01-20 10:02:44 +01:00
parent cd2c88b43b
commit d523c51e53
6 changed files with 180 additions and 97 deletions

View file

@ -1,10 +1,13 @@
use crate::light::{Command, Light, Status};
use crate::light::{Command, Light, Status, Update, Value};
use crossbeam_channel::RecvTimeoutError;
use lifxi::http::prelude::*;
use std::time::Duration;
pub struct Lifx {
client: Client,
updates: crossbeam_channel::Sender<Status>,
commands: crossbeam_channel::Receiver<Command>,
lights: Vec<Light>,
}
impl Lifx {
@ -17,10 +20,11 @@ impl Lifx {
client: Client::new(secret),
updates,
commands,
lights: vec![],
}
}
pub fn find_lights(&self) -> Vec<Light> {
pub fn get_lights(&self) -> Vec<Light> {
self.client
.select(Selector::All)
.list()
@ -30,6 +34,68 @@ impl Lifx {
.unwrap()
}
pub fn listen(&mut self) {
loop {
match self.commands.recv_timeout(Duration::from_secs(1)) {
Ok(command) => self.handle_command(command),
Err(RecvTimeoutError::Disconnected) => return,
Err(RecvTimeoutError::Timeout) => {}
}
self.update_lights();
}
}
fn update_lights(&mut self) {
let new_lights = self.get_lights();
// find changes
for new_light in &new_lights {
if let Some(old_light) = self.lights.iter().find(|x| new_light.id == x.id) {
self.find_diffs(old_light, new_light);
} else {
self.updates.send(Status::New(new_light.clone())).unwrap();
}
}
// find removed lamps
self.lights
.iter()
.filter(|o| new_lights.iter().find(|n| n.id == o.id).is_none())
.for_each(|l| {
self.updates.send(Status::Remove(l.label.clone())).unwrap();
});
self.lights = new_lights;
}
fn find_diffs(&self, old_light: &Light, new_light: &Light) {
if old_light.power != new_light.power {
self.updates
.send(Status::Update(Update::new(
&new_light.label,
Value::Power(new_light.power.clone()),
)))
.unwrap();
}
if (old_light.brightness - new_light.brightness).abs() < 0.01 {
self.updates
.send(Status::Update(Update::new(
&new_light.label,
Value::Brightness(new_light.brightness),
)))
.unwrap();
}
}
fn handle_command(&self, command: Command) {
match command.command {
Value::Power(val) => self.set_power(command.lampname, val == "on").unwrap(),
Value::Brightness(val) => self.set_brightness(command.lampname, val).unwrap(),
};
}
fn set_power(&self, id: String, state: bool) -> Result<(), lifxi::http::Error> {
self.client
.select(Selector::Id(id))

View file

@ -1,11 +1,11 @@
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, Clone)]
pub struct Color {
pub hue: f32,
pub saturation: f32,
pub kelvin: f32,
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, Clone)]
pub struct Light {
pub id: String,
pub label: String,
@ -15,8 +15,8 @@ pub struct Light {
pub brightness: f32,
}
const POWER: &str = "power";
const BRIGHTNESS: &str = "brightness";
pub const POWER: &str = "power";
pub const BRIGHTNESS: &str = "brightness";
pub enum Value {
Power(String),
@ -27,30 +27,24 @@ impl Value {
pub fn new(label: &str, value: Vec<u8>) -> Self {
match label {
POWER => Value::Power(String::from_utf8(value).unwrap()),
BRIGHTNESS => Value::Brightness(Self::vec_to_f32(value)),
BRIGHTNESS => Value::Brightness(vec_to_f32(value)),
_ => unimplemented!(),
}
}
fn vec_to_f32(vec: Vec<u8>) -> f32 {
assert!(vec.len() == 4);
let mut value_u32: u32 = 0;
for val in vec.clone() {
value_u32 = value_u32 << 8;
value_u32 = value_u32 | val as u32;
}
println!("{:?} -> {}", vec, value_u32);
f32::from_bits(value_u32)
}
pub fn unravel(self) -> (&'static str, Vec<u8>) {
match self {
Value::Power(val) => (POWER, val.into_bytes()),
Value::Brightness(val) => (BRIGHTNESS, Vec::new()),
Value::Brightness(val) => (BRIGHTNESS, (val as u32).to_ne_bytes().to_vec()),
}
}
}
fn vec_to_f32(value: Vec<u8>) -> f32 {
assert!(value.len() == 4);
u32::from_ne_bytes([value[0], value[1], value[2], value[3]]) as f32
}
pub struct Command {
pub lampname: String,
pub command: Value,
@ -61,6 +55,15 @@ pub struct Update {
pub status: Value,
}
impl Update {
pub fn new(lampname: &str, status: Value) -> Self {
Update {
lampname: lampname.to_owned(),
status,
}
}
}
pub enum Status {
Update(Update),
New(Light),

View file

@ -19,7 +19,7 @@ use std::thread;
pub const MQTT_ID: &str = "lifx-mqtt-bridge";
fn main() {
let matches = App::new("lifx-mqtt-bridge")
let matches = App::new(MQTT_ID)
.version("0.1")
.about("Lifx Mqtt Bridge")
.author("Buntpfotenkätzchen")
@ -38,9 +38,9 @@ fn main() {
.default_value("1883"),
)
.arg(
Arg::with_name("lifx_secret")
Arg::with_name("lifx-secret")
.short("s")
.long("lifx_secret")
.long("lifx-secret")
.required(true)
.takes_value(true),
)
@ -48,22 +48,22 @@ fn main() {
let host = matches.value_of("host").unwrap();
let port: u16 = matches.value_of("port").unwrap().parse().unwrap();
let lifx_secret = matches.value_of("lifx_secret").unwrap();
let lifx_secret = matches.value_of("lifx-secret").unwrap();
println!("Connecting to {}:{}", host, port);
let (s_commands, r_commands) = unbounded();
let (s_updates, r_updates) = unbounded();
let (mqtt_commands, mut mqtt_updates) = match mqtt::mqtt_connect(host, port, s_commands, r_updates)
{
Ok(mqtt) => mqtt,
Err(err) => panic!("Error connecting: {}", err),
};
let (mqtt_commands, mut mqtt_updates) =
match mqtt::mqtt_connect(host, port, s_commands, r_updates) {
Ok(mqtt) => mqtt,
Err(err) => panic!("Error connecting: {}", err),
};
let lifx_client = lifx::Lifx::new(lifx_secret, s_updates, r_commands);
let mut lifx_client = lifx::Lifx::new(lifx_secret, s_updates, r_commands);
thread::spawn(move || mqtt_commands.listen());
thread::spawn(move || mqtt_updates.listen());
loop {}
lifx_client.listen();
}

View file

@ -1,4 +1,4 @@
use crate::light::Status;
use crate::light::{Status, Update, Value, BRIGHTNESS, POWER};
use rumqtt;
use rumqtt::{MqttClient, QoS};
@ -18,40 +18,54 @@ impl MqttUpdates {
false,
format!("{}:{}", id, lampname),
)?;
let base_url = format!("{}/{}/", crate::MQTT_ID, lampname);
self.client.publish(
format!("{}/{}/status/connected", crate::MQTT_ID, lampname),
base_url.clone() + "status/connected",
QoS::AtLeastOnce,
false,
true,
"true",
)?;
self.client.subscribe(
format!("{}/{}/command/power", crate::MQTT_ID, lampname),
QoS::AtLeastOnce,
)?;
self.client.subscribe(
format!("{}/{}/command/brightness", crate::MQTT_ID, lampname),
QoS::AtLeastOnce,
)?;
self.client
.subscribe(base_url.clone() + "command/" + POWER, QoS::AtLeastOnce)?;
self.client
.subscribe(base_url.clone() + "command/" + BRIGHTNESS, QoS::AtLeastOnce)?;
Ok(())
}
pub fn listen(&mut self) {
while let Ok(status) = self.updates.recv() {
match status {
Status::New(light) => self.add_light(&light.id, &light.label).unwrap(),
Status::Remove(_name) => unimplemented!(),
Status::Update(update) => {
let (detail, value) = update.status.unravel();
self.client
.publish(
format!("{}/{}/status/{}", crate::MQTT_ID, update.lampname, detail),
QoS::AtLeastOnce,
true,
value,
)
.unwrap();
Status::New(light) => {
self.add_light(&light.id, &light.label).unwrap();
self.handle_update(Update::new(&light.label, Value::Power(light.power)));
self.handle_update(Update::new(
&light.label,
Value::Brightness(light.brightness),
));
}
Status::Remove(_name) => self
.client
.publish(
format!("{}/{}/status/connected", crate::MQTT_ID, _name),
QoS::AtLeastOnce,
true,
"false",
)
.unwrap(),
Status::Update(update) => self.handle_update(update),
}
}
}
fn handle_update(&mut self, update: Update) {
let (detail, value) = update.status.unravel();
self.client
.publish(
format!("{}/{}/status/{}", crate::MQTT_ID, update.lampname, detail),
QoS::AtLeastOnce,
true,
value,
)
.unwrap();
}
}