licht24/src/main.rs
2025-03-18 18:18:34 +01:00

181 lines
5.1 KiB
Rust

use log::{info, warn};
use std::{
sync::{
Arc,
atomic::{AtomicBool, AtomicU8, Ordering},
},
time::Duration,
};
use axum::{
Json, Router,
extract::State,
response::Html,
routing::{get, post},
};
use rumqttc::{AsyncClient, MqttOptions, QoS};
use serde::{Deserialize, Serialize};
use tokio::task;
struct Lights {
on: AtomicBool,
brightness: AtomicU8,
}
struct AppState {
mqtt_client: AsyncClient,
topics: Vec<String>,
lights_state: Lights,
}
#[derive(Serialize, Deserialize)]
struct LightsData {
on: bool,
brightness: Option<u8>,
}
const INDEX_HTML: &'static str = r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Licht 24 - Licht hier jetzt!</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="">
<script>
function post_brightness() {
fetch("/lights", {
method: "POST",
body: JSON.stringify({
on: true,
brightness: parseInt(document.getElementById("brightness").value)
}),
headers: {
"Content-type": "application/json; charset=UTF-8"
}
})
}
</script>
<style>
</style>
</head>
<body>
<div>
<h3>Helligkeit</h3>
<input type="range" id="brightness" name="brightness" min="1" max="255" onmouseup="post_brightness()" ontouchend="post_brightness()" />
</div>
</body>
</html>"#;
#[tokio::main]
async fn main() {
let mut mqttoptions = MqttOptions::new("licht24", "192.168.3.145", 1883);
mqttoptions.set_keep_alive(Duration::from_secs(5));
let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
let shared_state = Arc::new(AppState {
mqtt_client: client,
topics: vec![
String::from("zigbee2mqtt/sofaecke/decke_1/set"),
String::from("zigbee2mqtt/sofaecke/decke_2/set"),
String::from("zigbee2mqtt/sofaecke/decke_3/set"),
String::from("zigbee2mqtt/sitzecke/decke_1/set"),
String::from("zigbee2mqtt/sitzecke/decke_2/set"),
String::from("zigbee2mqtt/trinkregal/n_1/set"),
String::from("zigbee2mqtt/trinkregal/n_2/set"),
String::from("zigbee2mqtt/elektro_ecke/decke_1/set"),
String::from("zigbee2mqtt/elektro_ecke/decke_2/set"),
String::from("zigbee2mqtt/spind/set"),
String::from("zigbee2mqtt/3ddruck/wand/set"),
],
lights_state: Lights {
on: AtomicBool::new(true),
brightness: AtomicU8::new(100),
},
});
task::spawn(async move {
loop {
let notification = eventloop
.poll()
.await
.expect("Couldn't poll MQTT eventloop.");
println!("Received = {:?}", notification);
}
});
let app = Router::new()
.route("/", get(|| async { Html(INDEX_HTML) }))
.route("/lights", post(set_lights))
.with_state(shared_state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.expect("Unable to bind to port 3000.");
axum::serve(listener, app).await.unwrap();
}
async fn set_lights(
State(state): State<Arc<AppState>>,
Json(body): Json<LightsData>,
) -> Json<LightsData> {
if let Some(brightness) = body.brightness {
for topic in &state.topics {
match state
.mqtt_client
.publish(
topic,
QoS::AtMostOnce,
false,
format!("{{\"state\": \"ON\", \"brightness\": {0}}}", brightness),
)
.await
{
Ok(_) => {
state
.lights_state
.brightness
.store(brightness, Ordering::Relaxed);
state.lights_state.on.store(true, Ordering::Relaxed);
info!(
"Successfully published brightness level: {0} to topic: {topic}",
brightness
)
}
Err(err) => warn!("Unable to publish to topic: {topic} with error: {err}"),
}
}
} else {
for topic in &state.topics {
match state
.mqtt_client
.publish(
topic,
QoS::AtMostOnce,
false,
match body.on {
true => format!("{{\"state\": \"ON\"}}"),
false => format!("{{\"state\": \"OFF\"}}"),
},
)
.await
{
Ok(_) => {
state.lights_state.on.store(body.on, Ordering::Relaxed);
info!(
"Successfully published brightness state: {0} to topic: {topic}",
match body.on {
true => "ON",
false => "OFF",
},
)
}
Err(err) => warn!("Unable to publish to topic: {topic} with error: {err}"),
}
}
}
Json(body)
}