v0.13.0
This commit is contained in:
+2
-1
@@ -278,7 +278,7 @@ async fn list_home_assistant_groups(
|
||||
let groups = state.db.list_groups()?;
|
||||
let zones = state.db.list_zones()?;
|
||||
let devices = state.db.list_devices()?;
|
||||
let plan = engine::build_control_plan(&state).await?;
|
||||
let plan = engine::get_control_plan_snapshot(&state).await?;
|
||||
let settings = state.settings.read().await.clone();
|
||||
let mut output = Vec::with_capacity(groups.len());
|
||||
|
||||
@@ -288,6 +288,7 @@ async fn list_home_assistant_groups(
|
||||
.filter(|zone| group.zone_ids.iter().any(|zone_id| zone_id == &zone.id))
|
||||
.collect::<Vec<_>>();
|
||||
let planned_members = plan
|
||||
.plan
|
||||
.zones
|
||||
.iter()
|
||||
.filter(|zone| {
|
||||
|
||||
+2
-3
@@ -485,7 +485,6 @@ async fn history(
|
||||
}
|
||||
|
||||
async fn control_plan(State(state): State<AppState>) -> Result<Json<Value>, AppError> {
|
||||
Ok(Json(serde_json::to_value(
|
||||
engine::build_control_plan(&state).await?,
|
||||
)?))
|
||||
let snapshot = engine::get_control_plan_snapshot(&state).await?;
|
||||
Ok(Json(serde_json::to_value(snapshot.plan.as_ref())?))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
async fn home_assistant_snapshot(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let control_plan = engine::get_control_plan_snapshot(&state).await?;
|
||||
let groups = list_home_assistant_groups(State(state.clone())).await?.0;
|
||||
Ok(Json(json!({
|
||||
"devices": state.db.list_devices()?,
|
||||
"control_plan": control_plan.plan.as_ref(),
|
||||
"control_plan_revision": control_plan.revision,
|
||||
"groups": groups,
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HaTestRequest {
|
||||
entity_id: Option<String>,
|
||||
|
||||
@@ -20,6 +20,7 @@ async fn build_bootstrap(state: &AppState) -> Result<Value, AppError> {
|
||||
let online_count = devices.iter().filter(|value| value.online).count();
|
||||
let simulator_count = devices.iter().filter(|value| value.simulated).count();
|
||||
let (received_frames_total, received_frames_by_device) = state.gree.received_frame_stats();
|
||||
let control_plan = engine::get_control_plan_snapshot(state).await?;
|
||||
Ok(json!({
|
||||
"devices": devices,
|
||||
"zones": state.db.list_zones()?,
|
||||
@@ -30,6 +31,8 @@ async fn build_bootstrap(state: &AppState) -> Result<Value, AppError> {
|
||||
"access_tokens": state.db.list_api_tokens()?,
|
||||
"house": {"mode": settings.house_mode},
|
||||
"outdoor_temperature": *state.outdoor_temperature.read().await,
|
||||
"control_plan": control_plan.plan.as_ref(),
|
||||
"control_plan_revision": control_plan.revision,
|
||||
"system": {
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"uptime_seconds": state.started.elapsed().as_secs(),
|
||||
|
||||
+72
-12
@@ -14,21 +14,60 @@ async fn websocket(
|
||||
Ok(ws.on_upgrade(move |socket| websocket_loop(state, socket)))
|
||||
}
|
||||
|
||||
async fn websocket_loop(state: AppState, mut socket: WebSocket) {
|
||||
let initial = match build_bootstrap(&state).await {
|
||||
Ok(data) => json!({"event":"bootstrap","timestamp":Utc::now(),"data":data}),
|
||||
Err(err) => {
|
||||
json!({"event":"error","timestamp":Utc::now(),"data":{"message":err.to_string()}})
|
||||
fn control_plan_ws_message(snapshot: &crate::state::ControlPlanSnapshot) -> Value {
|
||||
json!({
|
||||
"event": "control_plan.updated",
|
||||
"timestamp": Utc::now(),
|
||||
"data": {
|
||||
"revision": snapshot.revision,
|
||||
"plan": snapshot.plan.as_ref(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_ws_bootstrap(state: &AppState, socket: &mut WebSocket) -> Result<Option<u64>, ()> {
|
||||
let (message, revision) = match build_bootstrap(state).await {
|
||||
Ok(data) => {
|
||||
let revision = data.get("control_plan_revision").and_then(Value::as_u64);
|
||||
(
|
||||
json!({"event":"bootstrap","timestamp":Utc::now(),"data":data}),
|
||||
revision,
|
||||
)
|
||||
}
|
||||
Err(err) => (
|
||||
json!({"event":"error","timestamp":Utc::now(),"data":{"message":err.to_string()}}),
|
||||
None,
|
||||
),
|
||||
};
|
||||
if socket
|
||||
.send(Message::Text(initial.to_string()))
|
||||
socket
|
||||
.send(Message::Text(message.to_string()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
.map_err(|_| ())?;
|
||||
Ok(revision)
|
||||
}
|
||||
|
||||
async fn websocket_loop(state: AppState, mut socket: WebSocket) {
|
||||
// Subscribe before building the bootstrap so state changes during bootstrap generation are
|
||||
// queued and can be applied immediately after the first frame.
|
||||
let mut receiver = state.events.subscribe();
|
||||
let mut control_plan = state.subscribe_control_plan();
|
||||
|
||||
let bootstrap_revision = match send_ws_bootstrap(&state, &mut socket).await {
|
||||
Ok(revision) => revision,
|
||||
Err(()) => return,
|
||||
};
|
||||
let mut last_control_plan_revision = bootstrap_revision;
|
||||
|
||||
// If the watch value is exactly the plan embedded in bootstrap, mark it seen to avoid a
|
||||
// duplicate control_plan.updated frame. A newer revision remains pending and is sent below.
|
||||
let current_revision = control_plan
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|snapshot| snapshot.revision);
|
||||
if current_revision == bootstrap_revision {
|
||||
control_plan.borrow_and_update();
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = receiver.recv() => {
|
||||
@@ -38,10 +77,31 @@ async fn websocket_loop(state: AppState, mut socket: WebSocket) {
|
||||
if socket.send(Message::Text(text)).await.is_err() { break; }
|
||||
}
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
tracing::warn!(skipped, "websocket client lagged; sending full bootstrap resync");
|
||||
// Drop the retained stale backlog before taking the replacement snapshot.
|
||||
// Events created while bootstrap is built are queued on this fresh receiver.
|
||||
receiver = state.events.subscribe();
|
||||
match send_ws_bootstrap(&state, &mut socket).await {
|
||||
Ok(revision) => last_control_plan_revision = revision,
|
||||
Err(()) => break,
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
changed = control_plan.changed() => {
|
||||
if changed.is_err() { break; }
|
||||
let snapshot = control_plan.borrow_and_update().clone();
|
||||
if let Some(snapshot) = snapshot {
|
||||
if last_control_plan_revision == Some(snapshot.revision) {
|
||||
continue;
|
||||
}
|
||||
let text = control_plan_ws_message(snapshot.as_ref()).to_string();
|
||||
if socket.send(Message::Text(text)).await.is_err() { break; }
|
||||
last_control_plan_revision = Some(snapshot.revision);
|
||||
}
|
||||
}
|
||||
message = socket.next() => {
|
||||
match message {
|
||||
Some(Ok(Message::Ping(value))) => { if socket.send(Message::Pong(value)).await.is_err() { break; } }
|
||||
|
||||
Reference in New Issue
Block a user