testing paralell
This commit is contained in:
50
Cargo.lock
generated
50
Cargo.lock
generated
@@ -33,6 +33,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"config",
|
||||
"ctrlc",
|
||||
"futures",
|
||||
"grim-rs",
|
||||
"image",
|
||||
"image-compare",
|
||||
@@ -861,6 +862,21 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
@@ -868,6 +884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -876,6 +893,34 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
@@ -894,8 +939,13 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
@@ -7,6 +7,7 @@ edition = "2021"
|
||||
anyhow = "1.0"
|
||||
config = "0.15.21"
|
||||
ctrlc = "3.5.2"
|
||||
futures = "0.3"
|
||||
grim-rs = "0.1.6"
|
||||
image = "0.25.10"
|
||||
image-compare = "0.5.0"
|
||||
|
||||
100
README.md
100
README.md
@@ -78,3 +78,103 @@ nix develop -c cargo run --release
|
||||
3. **Analyze:** Calculates the dominant color of the current frame using the Oklab color space.
|
||||
4. **Smooth:** Blends the new dominant color with the previous one based on the `smoothing` factor.
|
||||
5. **Update:** Constructs a highly specific JSON payload for Home Assistant, ensuring white channels and brightness are preserved, and sends it asynchronously via `reqwest`.
|
||||
|
||||
|
||||
|
||||
# TODO: Parallel Event Loop Restructuring
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Shared State │
|
||||
│ │
|
||||
│ current_screenshot ← Latest captured (screenshot loop) │
|
||||
│ active_screenshot ← Screenshot that produced target_color│
|
||||
│ target_color ← Color calculated from active_screenshot│
|
||||
│ active_color ← Smoothed color sent to lights │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Data Flow:
|
||||
|
||||
Screenshot Loop (screenshot_fps) Color Calc (on-demand)
|
||||
│ │
|
||||
▼ ▼
|
||||
[current_screenshot] [target_color]
|
||||
│ │
|
||||
│ diff(current, active) │
|
||||
│ > min_diff_percent? │
|
||||
▼ │
|
||||
[active_screenshot] ◄───────────────────────┘
|
||||
│ │
|
||||
│ Smoothing Loop (target_fps)│
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ [active_color] ◄─────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
Light Update (parallel to all lights)
|
||||
```
|
||||
|
||||
## Implementation Tasks
|
||||
|
||||
### Phase 1: Settings Update (`settings.rs`)
|
||||
- Add `screenshot_fps` config option (default: 3)
|
||||
- Remove `transition` option (smoothing handles transitions)
|
||||
|
||||
### Phase 2: Create Shared State (`state.rs`)
|
||||
```rust
|
||||
pub struct AppState {
|
||||
pub current_screenshot: RwLock<Option<Arc<Screenshot>>>,
|
||||
pub active_screenshot: RwLock<Option<Arc<Screenshot>>>,
|
||||
pub target_color: RwLock<Option<Rgb<f32>>>,
|
||||
pub active_color: RwLock<Rgb<f32>>,
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Parallel Event Loop (`eventloop.rs`)
|
||||
|
||||
**Thread 1 - Screenshot Loop** (std::thread, runs at `screenshot_fps`)
|
||||
- Capture screenshot → store in `current_screenshot`
|
||||
- Compare `current_screenshot` vs `active_screenshot`
|
||||
- If diff > `min_diff_percent`:
|
||||
- Copy `current_screenshot` → `active_screenshot`
|
||||
- Spawn color calc task
|
||||
|
||||
**On-demand - Color Calculation** (tokio::spawn)
|
||||
- Calculate dominant color from `active_screenshot`
|
||||
- Update `target_color`
|
||||
|
||||
**Thread 2 - Smoothing & Light Update** (tokio, runs at `target_fps`)
|
||||
- Lerp `active_color` toward `target_color` using `smoothing` factor
|
||||
- Send `active_color` to all lights **in parallel**
|
||||
|
||||
### Phase 4: Parallel Light Updates (`homeassistant.rs`)
|
||||
- Refactor `set_lights_color` to update single light, return future
|
||||
- Create `set_lights_color_parallel` using `futures::future::join_all`
|
||||
|
||||
## Concurrency Strategy
|
||||
|
||||
| Resource | Lock Type | Writer | Readers |
|
||||
|---------------------|-----------|---------------------|----------------------------|
|
||||
| current_screenshot | RwLock | Screenshot loop | Diff check |
|
||||
| active_screenshot | RwLock | Screenshot loop | Color calc |
|
||||
| target_color | RwLock | Color calc task | Smoothing loop |
|
||||
| active_color | RwLock | Smoothing loop | Light update |
|
||||
|
||||
Each field has a single writer - no complex synchronization needed.
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
| File | Changes |
|
||||
|-------------------|----------------------------------------------|
|
||||
| `settings.rs` | Add `screenshot_fps`, remove `transition` |
|
||||
| `state.rs` | **New** - Shared state struct |
|
||||
| `eventloop.rs` | **Rewrite** - Multi-threaded architecture |
|
||||
| `homeassistant.rs`| Add parallel light update method |
|
||||
| `main.rs` | Wire up new state, pass to eventloop |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
64
config.toml
64
config.toml
@@ -1,31 +1,43 @@
|
||||
# Configuration for Ambilight Home Assistant Integration
|
||||
# # Configuration for Ambilight Home Assistant Integration
|
||||
#
|
||||
# # The base URL of your Home Assistant instance.
|
||||
# # Example: "http://192.168.1.100:8123" or "https://ha.yourdomain.com"
|
||||
# ha_url = "http://192.168.1.238:8123"
|
||||
#
|
||||
# # A Long-Lived Access Token from Home Assistant.
|
||||
# # You can generate this in Home Assistant by going to your user profile.
|
||||
# ha_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI0YjNjMmYxNGE3NWM0YmJjYmNhYzA0YWFmYTlhOTVkNCIsImlhdCI6MTc3MzUyOTcwNCwiZXhwIjoyMDg4ODg5NzA0fQ.I923kml1zWqYYZk-0JSunexbo5NKcgehhwmG-T8jBcg"
|
||||
#
|
||||
# # Target updates per second (FPS)
|
||||
# target_fps = 3
|
||||
#
|
||||
# # Color smoothing factor (0.0 to 1.0)
|
||||
# # 0.0 = no smoothing (instant updates)
|
||||
# # 1.0 = completely static (no updates)
|
||||
# smoothing = 0.1
|
||||
#
|
||||
# # Specific lights to control. If empty, all supported color lights are used.
|
||||
# # Example: ["light.living_room", "light.bedroom"]
|
||||
# lights = []
|
||||
#
|
||||
# # Restore lights to their original state/color when the program exits.
|
||||
# restore_on_exit = true
|
||||
#
|
||||
# # Time (in seconds) it takes to fade to the new color. 0.0 means instant.
|
||||
# transition = 0.1
|
||||
#
|
||||
# # Minimum percentage of difference required between frames to trigger an update (e.g. 1.0 = 1%)
|
||||
# # Helps with flickering on static images, from the non deterministic color algorithm.
|
||||
# min_diff_percent = 5.0
|
||||
|
||||
# The base URL of your Home Assistant instance.
|
||||
# Example: "http://192.168.1.100:8123" or "https://ha.yourdomain.com"
|
||||
ha_url = "http://192.168.1.238:8123"
|
||||
|
||||
# A Long-Lived Access Token from Home Assistant.
|
||||
# You can generate this in Home Assistant by going to your user profile.
|
||||
ha_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI0YjNjMmYxNGE3NWM0YmJjYmNhYzA0YWFmYTlhOTVkNCIsImlhdCI6MTc3MzUyOTcwNCwiZXhwIjoyMDg4ODg5NzA0fQ.I923kml1zWqYYZk-0JSunexbo5NKcgehhwmG-T8jBcg"
|
||||
|
||||
# Target updates per second (FPS)
|
||||
target_fps = 3
|
||||
|
||||
# Color smoothing factor (0.0 to 1.0)
|
||||
# 0.0 = no smoothing (instant updates)
|
||||
# 1.0 = completely static (no updates)
|
||||
smoothing = 0.1
|
||||
|
||||
# Specific lights to control. If empty, all supported color lights are used.
|
||||
# Example: ["light.living_room", "light.bedroom"]
|
||||
lights = []
|
||||
|
||||
# Restore lights to their original state/color when the program exits.
|
||||
ha_url = "https://homeassistant.pvv.ntnu.no:8123"
|
||||
ha_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3ZmIxMzQ4NzE5ZmU0MmMyYTA5M2JiNWU2NjQ2OWJmYSIsImlhdCI6MTc3MzY2MzQwNCwiZXhwIjoyMDg5MDIzNDA0fQ.JKn8hPvK7aPS-SuXKKEREk7vva-PGXehLUH7ijoSbjQ"
|
||||
target_fps = 15.0
|
||||
smoothing = 0.6
|
||||
lights = [ "light.fargelys" ]
|
||||
restore_on_exit = true
|
||||
|
||||
# Time (in seconds) it takes to fade to the new color. 0.0 means instant.
|
||||
transition = 0.1
|
||||
|
||||
# Minimum percentage of difference required between frames to trigger an update (e.g. 1.0 = 1%)
|
||||
# Helps with flickering on static images, from the non deterministic color algorithm.
|
||||
min_diff_percent = 5.0
|
||||
transition = 0.0
|
||||
min_diff_percent = 10.0
|
||||
|
||||
187
src/eventloop.rs
187
src/eventloop.rs
@@ -1,93 +1,148 @@
|
||||
use crate::homeassistant::HaClient;
|
||||
use crate::screenshot;
|
||||
use crate::settings::Settings;
|
||||
use crate::state::AppState;
|
||||
use rgb::RGB;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub async fn run_loop(
|
||||
settings: &Settings,
|
||||
ha_client: &HaClient,
|
||||
target_lights: &[String],
|
||||
pub fn run_loop(
|
||||
settings: Arc<Settings>,
|
||||
ha_client: Arc<HaClient>,
|
||||
target_lights: Arc<Vec<String>>,
|
||||
state: Arc<AppState>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
mut exit_rx: mpsc::Receiver<()>,
|
||||
) {
|
||||
let fps = settings.target_fps.max(1);
|
||||
let target_duration = Duration::from_millis((1000 / fps) as u64);
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
let mut current_color: Option<(f32, f32, f32)> = None;
|
||||
let mut last_screenshot: Option<screenshot::Screenshot> = None;
|
||||
let screenshot_fps = settings.screenshot_fps.max(1);
|
||||
let target_fps = settings.target_fps.max(1);
|
||||
let screenshot_interval = Duration::from_millis((1000 / screenshot_fps) as u64);
|
||||
let target_interval = Duration::from_millis((1000 / target_fps) as u64);
|
||||
let smoothing = settings.smoothing.clamp(0.0, 1.0);
|
||||
let min_diff_percent = settings.min_diff_percent;
|
||||
|
||||
println!("Starting Ambilight loop at {} FPS...", fps);
|
||||
println!(
|
||||
"Starting Ambilight loop (screenshot: {} FPS, light update: {} FPS)...",
|
||||
screenshot_fps, target_fps
|
||||
);
|
||||
|
||||
loop {
|
||||
let start_time = Instant::now();
|
||||
let is_running_screenshot = is_running.clone();
|
||||
let state_screenshot = state.clone();
|
||||
let settings_screenshot = settings.clone();
|
||||
|
||||
if is_running.load(Ordering::Relaxed) {
|
||||
match screenshot::get_screenshot(0) {
|
||||
Ok(current_screenshot) => {
|
||||
let mut should_update = true;
|
||||
let screenshot_handle = thread::spawn(move || {
|
||||
loop {
|
||||
let start = Instant::now();
|
||||
|
||||
if let Some(prev) = &last_screenshot {
|
||||
let diff = current_screenshot.diff_percent(prev);
|
||||
if diff < settings.min_diff_percent {
|
||||
should_update = false;
|
||||
if is_running_screenshot.load(Ordering::Relaxed) {
|
||||
if let Ok(current) = screenshot::get_screenshot(0) {
|
||||
let current_arc = Arc::new(current);
|
||||
|
||||
let should_update = {
|
||||
let active = state_screenshot.active_screenshot.read().unwrap();
|
||||
match active.as_ref() {
|
||||
None => true,
|
||||
Some(active_screenshot) => {
|
||||
let diff = current_arc.diff_percent(active_screenshot);
|
||||
diff >= settings_screenshot.min_diff_percent
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if should_update {
|
||||
let color = current_screenshot.dominant_color();
|
||||
let (new_r, new_g, new_b) =
|
||||
(color.r as f32, color.g as f32, color.b as f32);
|
||||
|
||||
let (r, g, b) = match current_color {
|
||||
None => (new_r, new_g, new_b),
|
||||
Some((cr, cg, cb)) => {
|
||||
let s_factor = settings.smoothing.clamp(0.0, 1.0);
|
||||
(
|
||||
cr * s_factor + new_r * (1.0 - s_factor),
|
||||
cg * s_factor + new_g * (1.0 - s_factor),
|
||||
cb * s_factor + new_b * (1.0 - s_factor),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
current_color = Some((r, g, b));
|
||||
let (final_r, final_g, final_b) =
|
||||
(r.round() as u8, g.round() as u8, b.round() as u8);
|
||||
|
||||
if let Err(e) = ha_client
|
||||
.set_lights_color(
|
||||
target_lights,
|
||||
final_r,
|
||||
final_g,
|
||||
final_b,
|
||||
settings.transition,
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to update lights: {}", e);
|
||||
let mut current_guard = state_screenshot.current_screenshot.write().unwrap();
|
||||
*current_guard = Some(current_arc.clone());
|
||||
}
|
||||
{
|
||||
let mut active_guard = state_screenshot.active_screenshot.write().unwrap();
|
||||
*active_guard = Some(current_arc);
|
||||
}
|
||||
|
||||
last_screenshot = Some(current_screenshot);
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Screenshot error: {}", e),
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let sleep_duration = screenshot_interval.saturating_sub(elapsed);
|
||||
thread::sleep(sleep_duration);
|
||||
}
|
||||
});
|
||||
|
||||
let state_smoothing = state.clone();
|
||||
let ha_client_smoothing = ha_client.clone();
|
||||
let target_lights_smoothing = target_lights.clone();
|
||||
let is_running_smoothing = is_running.clone();
|
||||
|
||||
rt.block_on(async {
|
||||
let mut last_calc_time = Instant::now();
|
||||
let calc_interval = Duration::from_millis(50);
|
||||
|
||||
loop {
|
||||
let start = Instant::now();
|
||||
|
||||
if is_running_smoothing.load(Ordering::Relaxed) {
|
||||
if start.duration_since(last_calc_time) >= calc_interval {
|
||||
let active_screenshot = {
|
||||
let guard = state_smoothing.active_screenshot.read().unwrap();
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
if let Some(screenshot) = active_screenshot {
|
||||
let color = screenshot.dominant_color();
|
||||
let new_target = RGB::new(color.r as f32, color.g as f32, color.b as f32);
|
||||
|
||||
{
|
||||
let mut target_guard = state_smoothing.target_color.write().unwrap();
|
||||
*target_guard = Some(new_target);
|
||||
}
|
||||
}
|
||||
last_calc_time = start;
|
||||
}
|
||||
|
||||
{
|
||||
let target_color = {
|
||||
let guard = state_smoothing.target_color.read().unwrap();
|
||||
*guard
|
||||
};
|
||||
|
||||
if let Some(target) = target_color {
|
||||
let mut active_guard = state_smoothing.active_color.write().unwrap();
|
||||
active_guard.r = active_guard.r * smoothing + target.r * (1.0 - smoothing);
|
||||
active_guard.g = active_guard.g * smoothing + target.g * (1.0 - smoothing);
|
||||
active_guard.b = active_guard.b * smoothing + target.b * (1.0 - smoothing);
|
||||
}
|
||||
}
|
||||
|
||||
let (r, g, b) = {
|
||||
let guard = state_smoothing.active_color.read().unwrap();
|
||||
(guard.r.round() as u8, guard.g.round() as u8, guard.b.round() as u8)
|
||||
};
|
||||
|
||||
if let Err(e) = ha_client_smoothing
|
||||
.set_lights_color_parallel(&target_lights_smoothing, r, g, b)
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to update lights: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let sleep_duration = target_interval.saturating_sub(elapsed);
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(sleep_duration) => {}
|
||||
_ = exit_rx.recv() => {
|
||||
println!("\nExit signal received, stopping loop...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sleep to maintain target FPS, saturating_sub prevents negative duration panics if we run slow
|
||||
let elapsed = start_time.elapsed();
|
||||
let sleep_duration = target_duration.saturating_sub(elapsed);
|
||||
|
||||
tokio::select! {
|
||||
_ = sleep(sleep_duration) => {}
|
||||
_ = exit_rx.recv() => {
|
||||
println!("\nExit signal received, stopping loop...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = screenshot_handle.join();
|
||||
}
|
||||
|
||||
@@ -90,77 +90,98 @@ impl HaClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_lights_color(
|
||||
pub async fn set_light_color(
|
||||
&self,
|
||||
entity_id: &str,
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
states: &[HaState],
|
||||
) -> Result<(), Error> {
|
||||
let url = format!("{}/api/services/light/turn_on", self.base_url);
|
||||
|
||||
let mut payload = json!({
|
||||
"entity_id": entity_id,
|
||||
});
|
||||
|
||||
if let Some(state) = states.iter().find(|s| s.entity_id == entity_id) {
|
||||
if let Some(brightness) = state.attributes.get("brightness") {
|
||||
payload["brightness"] = brightness.clone();
|
||||
}
|
||||
|
||||
let color_mode = state
|
||||
.attributes
|
||||
.get("color_mode")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
match color_mode {
|
||||
"rgbw" => {
|
||||
let w = state
|
||||
.attributes
|
||||
.get("rgbw_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(3))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
payload["rgbw_color"] = json!([r, g, b, w]);
|
||||
}
|
||||
"rgbww" => {
|
||||
let cw = state
|
||||
.attributes
|
||||
.get("rgbww_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(3))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
let ww = state
|
||||
.attributes
|
||||
.get("rgbww_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(4))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
payload["rgbww_color"] = json!([r, g, b, cw, ww]);
|
||||
}
|
||||
_ => {
|
||||
payload["rgb_color"] = json!([r, g, b]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payload["rgb_color"] = json!([r, g, b]);
|
||||
}
|
||||
|
||||
self.client
|
||||
.post(&url)
|
||||
.bearer_auth(&self.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_lights_color_parallel(
|
||||
&self,
|
||||
entity_ids: &[String],
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
transition: f32,
|
||||
) -> Result<(), Error> {
|
||||
let states = self.get_lights().await.unwrap_or_default();
|
||||
let url = format!("{}/api/services/light/turn_on", self.base_url);
|
||||
|
||||
for entity_id in entity_ids {
|
||||
let mut payload = json!({
|
||||
"entity_id": entity_id,
|
||||
"transition": transition,
|
||||
});
|
||||
let futures: Vec<_> = entity_ids
|
||||
.iter()
|
||||
.map(|entity_id| self.set_light_color(entity_id, r, g, b, &states))
|
||||
.collect();
|
||||
|
||||
if let Some(state) = states.iter().find(|s| s.entity_id == *entity_id) {
|
||||
if let Some(brightness) = state.attributes.get("brightness") {
|
||||
payload["brightness"] = brightness.clone();
|
||||
}
|
||||
let results = futures::future::join_all(futures).await;
|
||||
|
||||
let color_mode = state
|
||||
.attributes
|
||||
.get("color_mode")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
match color_mode {
|
||||
"rgbw" => {
|
||||
let w = state
|
||||
.attributes
|
||||
.get("rgbw_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(3))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
payload["rgbw_color"] = json!([r, g, b, w]);
|
||||
}
|
||||
"rgbww" => {
|
||||
let cw = state
|
||||
.attributes
|
||||
.get("rgbww_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(3))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
let ww = state
|
||||
.attributes
|
||||
.get("rgbww_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(4))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
payload["rgbww_color"] = json!([r, g, b, cw, ww]);
|
||||
}
|
||||
_ => {
|
||||
payload["rgb_color"] = json!([r, g, b]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payload["rgb_color"] = json!([r, g, b]);
|
||||
for result in results {
|
||||
if let Err(e) = result {
|
||||
eprintln!("Failed to update light: {}", e);
|
||||
}
|
||||
|
||||
let _ = self
|
||||
.client
|
||||
.post(&url)
|
||||
.bearer_auth(&self.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
39
src/main.rs
39
src/main.rs
@@ -2,6 +2,7 @@ mod eventloop;
|
||||
mod homeassistant;
|
||||
mod screenshot;
|
||||
mod settings;
|
||||
mod state;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -48,32 +49,34 @@ fn main() {
|
||||
let is_running = Arc::new(AtomicBool::new(true));
|
||||
let (exit_tx, exit_rx) = tokio::sync::mpsc::channel(1);
|
||||
|
||||
// Leak variables to give them a 'static lifetime for the thread
|
||||
let settings_ref = &*Box::leak(Box::new(settings));
|
||||
let ha_client_ref = &*Box::leak(Box::new(ha_client));
|
||||
let target_lights_ref = &*Box::leak(Box::new(target_lights));
|
||||
let settings_arc = Arc::new(settings);
|
||||
let ha_client_arc = Arc::new(ha_client);
|
||||
let target_lights_arc = Arc::new(target_lights);
|
||||
let state_arc = Arc::new(state::AppState::new());
|
||||
|
||||
let is_running_clone = is_running.clone();
|
||||
let settings_arc_clone = settings_arc.clone();
|
||||
let ha_client_arc_clone = ha_client_arc.clone();
|
||||
let target_lights_arc_clone = target_lights_arc.clone();
|
||||
|
||||
// Spawn the main application event loop on a separate thread
|
||||
let app_thread = std::thread::spawn(move || {
|
||||
let rt2 = tokio::runtime::Runtime::new().unwrap();
|
||||
rt2.block_on(eventloop::run_loop(
|
||||
settings_ref,
|
||||
ha_client_ref,
|
||||
target_lights_ref,
|
||||
eventloop::run_loop(
|
||||
settings_arc_clone,
|
||||
ha_client_arc_clone,
|
||||
target_lights_arc_clone,
|
||||
state_arc,
|
||||
is_running_clone,
|
||||
exit_rx,
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
// Create the tray application
|
||||
let mut tray = TrayItem::new("Ambiligth", IconSource::Resource("")).unwrap();
|
||||
|
||||
let is_running_toggle = is_running.clone();
|
||||
let restore_on_toggle = settings_ref.restore_on_exit;
|
||||
let ha_client_toggle = ha_client_ref;
|
||||
let target_lights_toggle = target_lights_ref;
|
||||
let restore_on_toggle = settings_arc.restore_on_exit;
|
||||
let ha_client_toggle = ha_client_arc.clone();
|
||||
let target_lights_toggle = target_lights_arc.clone();
|
||||
let rt_handle_toggle = rt_handle.clone();
|
||||
|
||||
tray.add_menu_item("Toggle Ambilight", move || {
|
||||
@@ -98,7 +101,7 @@ fn main() {
|
||||
println!("Creating scene snapshot for restore on exit...");
|
||||
if let Err(e) = rt_handle_toggle.block_on(
|
||||
ha_client_toggle
|
||||
.create_scene_snapshot("ambilight_restore", target_lights_toggle),
|
||||
.create_scene_snapshot("ambilight_restore", &*target_lights_toggle),
|
||||
) {
|
||||
eprintln!("Failed to create scene snapshot: {}", e);
|
||||
}
|
||||
@@ -121,16 +124,14 @@ fn main() {
|
||||
})
|
||||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
// Block the main thread until a quit signal is received
|
||||
let _ = sync_exit_rx.recv();
|
||||
println!("Shutting down...");
|
||||
|
||||
// Stop the event loop
|
||||
let _ = exit_tx.try_send(());
|
||||
|
||||
if settings_ref.restore_on_exit {
|
||||
if settings_arc.restore_on_exit {
|
||||
println!("Restoring lights to original state...");
|
||||
if let Err(e) = rt.block_on(ha_client_ref.turn_on_scene("scene.ambilight_restore")) {
|
||||
if let Err(e) = rt.block_on(ha_client_arc.turn_on_scene("scene.ambilight_restore")) {
|
||||
eprintln!("Failed to restore scene: {}", e);
|
||||
} else {
|
||||
println!("Lights restored successfully.");
|
||||
|
||||
@@ -12,6 +12,9 @@ pub struct Settings {
|
||||
#[serde(default = "default_target_fps")]
|
||||
pub target_fps: u32,
|
||||
|
||||
#[serde(default = "default_screenshot_fps")]
|
||||
pub screenshot_fps: u32,
|
||||
|
||||
#[serde(default = "default_smoothing")]
|
||||
pub smoothing: f32,
|
||||
|
||||
@@ -21,9 +24,6 @@ pub struct Settings {
|
||||
#[serde(default = "default_restore_on_exit")]
|
||||
pub restore_on_exit: bool,
|
||||
|
||||
#[serde(default = "default_transition")]
|
||||
pub transition: f32,
|
||||
|
||||
#[serde(default = "default_min_diff_percent")]
|
||||
pub min_diff_percent: f32,
|
||||
}
|
||||
@@ -32,10 +32,6 @@ fn default_min_diff_percent() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
fn default_transition() -> f32 {
|
||||
0.5
|
||||
}
|
||||
|
||||
fn default_restore_on_exit() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -44,6 +40,10 @@ fn default_target_fps() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_screenshot_fps() -> u32 {
|
||||
3
|
||||
}
|
||||
|
||||
fn default_smoothing() -> f32 {
|
||||
0.0
|
||||
}
|
||||
|
||||
27
src/state.rs
Normal file
27
src/state.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use crate::screenshot::Screenshot;
|
||||
use rgb::RGB;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
pub struct AppState {
|
||||
pub current_screenshot: RwLock<Option<Arc<Screenshot>>>,
|
||||
pub active_screenshot: RwLock<Option<Arc<Screenshot>>>,
|
||||
pub target_color: RwLock<Option<RGB<f32>>>,
|
||||
pub active_color: RwLock<RGB<f32>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_screenshot: RwLock::new(None),
|
||||
active_screenshot: RwLock::new(None),
|
||||
target_color: RwLock::new(None),
|
||||
active_color: RwLock::new(RGB::new(0.0, 0.0, 0.0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user