5 changed files with 536 additions and 10 deletions
@ -0,0 +1,146 @@
@@ -0,0 +1,146 @@
|
||||
//! Identifying probed pixels and submitting them to Home Assistant.
|
||||
|
||||
use crate::config::Probe; |
||||
use crate::{Configuration, DecodedImage}; |
||||
use anyhow::anyhow; |
||||
use image::Pixel; |
||||
use log::{debug, info, warn}; |
||||
use reqwest::Client; |
||||
use serde::{Deserialize, Serialize}; |
||||
use std::collections::HashMap; |
||||
use std::time::{Duration, Instant}; |
||||
use tokio::sync::mpsc::Receiver; |
||||
|
||||
pub(crate) struct Prober { |
||||
pub(crate) rx: Receiver<DecodedImage>, |
||||
pub(crate) config: Configuration, |
||||
pub(crate) reqwest: Client, |
||||
} |
||||
|
||||
#[derive(Serialize)] |
||||
struct HassAttributes { |
||||
icon: String, |
||||
friendly_name: String, |
||||
device_class: String, |
||||
unique_id: String, |
||||
} |
||||
|
||||
#[derive(Serialize)] |
||||
struct HassPayload { |
||||
state: &'static str, |
||||
attributes: HassAttributes, |
||||
} |
||||
|
||||
#[derive(Deserialize)] |
||||
struct HassStateReply { |
||||
state: String, |
||||
} |
||||
|
||||
impl Prober { |
||||
async fn state_get(&self, probe: &Probe) -> anyhow::Result<String> { |
||||
let resp = self |
||||
.reqwest |
||||
.get(format!( |
||||
"{}/api/states/binary_sensor.{}", |
||||
self.config.home_assistant_uri, probe.unique_id |
||||
)) |
||||
.header( |
||||
"Authorization", |
||||
format!("Bearer {}", self.config.home_assistant_token), |
||||
) |
||||
.send() |
||||
.await?; |
||||
let ret: HassStateReply = resp.json().await?; |
||||
Ok(ret.state) |
||||
} |
||||
async fn state_set(&self, probe: &Probe, state: bool) -> anyhow::Result<()> { |
||||
self.reqwest |
||||
.post(format!( |
||||
"{}/api/states/binary_sensor.{}", |
||||
self.config.home_assistant_uri, probe.unique_id |
||||
)) |
||||
.header( |
||||
"Authorization", |
||||
format!("Bearer {}", self.config.home_assistant_token), |
||||
) |
||||
.header("Content-Type", "application/json") |
||||
.json(&HassPayload { |
||||
state: if state { "on" } else { "off" }, |
||||
attributes: HassAttributes { |
||||
icon: probe.icon.clone(), |
||||
friendly_name: probe.friendly_name.clone(), |
||||
device_class: probe.device_class.clone(), |
||||
unique_id: format!("binary_sensor.{}", probe.unique_id), |
||||
}, |
||||
}) |
||||
.send() |
||||
.await?; |
||||
Ok(()) |
||||
} |
||||
pub(crate) async fn run(mut self) -> anyhow::Result<()> { |
||||
// we use an Option to discern "not observed yet" from "off"
|
||||
let mut states: HashMap<String, (Option<bool>, Probe)> = HashMap::new(); |
||||
for probe in self.config.probes.iter() { |
||||
if states.contains_key(&probe.unique_id) { |
||||
return Err(anyhow!( |
||||
"Multiple probes have the unique ID {}", |
||||
probe.unique_id |
||||
)); |
||||
} |
||||
states.insert(probe.unique_id.clone(), (None, probe.clone())); |
||||
} |
||||
let retrans_dur = Duration::from_secs(self.config.state_retrans_interval_sec); |
||||
let mut next_retrans = Instant::now() + retrans_dur; |
||||
while let Some(i) = self.rx.recv().await { |
||||
for (id, (state, probe)) in states.iter_mut() { |
||||
let pixel_luma = i.get_pixel(probe.x, probe.y).to_luma()[0]; |
||||
let new_state = pixel_luma >= self.config.on_luma; |
||||
debug!( |
||||
"{}: ({}, {}) = {} ({})", |
||||
id, probe.x, probe.y, pixel_luma, new_state |
||||
); |
||||
if *state != Some(new_state) { |
||||
*state = Some(new_state); |
||||
info!( |
||||
"Probe {} ({}) detected as {}", |
||||
id, |
||||
probe.friendly_name, |
||||
if new_state { "ON" } else { "OFF" } |
||||
); |
||||
if let Err(e) = self.state_set(probe, new_state).await { |
||||
warn!("Failed to update Home Assistant: {}", e); |
||||
} |
||||
} |
||||
} |
||||
if next_retrans < Instant::now() { |
||||
debug!("Doing periodic state check"); |
||||
for (id, (state, probe)) in states.iter() { |
||||
if let Some(state) = state { |
||||
match self.state_get(probe).await { |
||||
Ok(hass) => { |
||||
let expected = if *state { "on" } else { "off" }; |
||||
debug!("{}: hass {} expected {}", id, hass, expected); |
||||
if hass == expected { |
||||
continue; |
||||
} |
||||
warn!( |
||||
"Probe {} ({}) is {}, but Home Assistant thinks it's {}!", |
||||
id, probe.friendly_name, expected, hass |
||||
); |
||||
if let Err(e) = self.state_set(probe, *state).await { |
||||
warn!("Failed to retransmit to Home Assistant: {}", e); |
||||
} |
||||
} |
||||
Err(e) => { |
||||
warn!("Failed to get state for {} from Home Assistant: {}", id, e); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
next_retrans = Instant::now() + retrans_dur; |
||||
} |
||||
} |
||||
debug!("probe loop stopped"); |
||||
Ok(()) |
||||
} |
||||
} |
Loading…
Reference in new issue