v0.12.0-preety_code

This commit is contained in:
Mateusz Gruszczyński
2026-09-03 15:33:37 +02:00
parent be67932b47
commit 07be4b85d9
87 changed files with 11691 additions and 3172 deletions
+118 -43
View File
@@ -1,8 +1,16 @@
async fn send_command_locked(state: &AppState, device_id: &str, command: DeviceCommand) -> Result<Device, AppError> {
async fn send_command_locked(
state: &AppState,
device_id: &str,
command: DeviceCommand,
) -> Result<Device, AppError> {
send_command_locked_inner(state, device_id, command, true, true).await
}
async fn send_command_locked_forced(state: &AppState, device_id: &str, command: DeviceCommand) -> Result<Device, AppError> {
async fn send_command_locked_forced(
state: &AppState,
device_id: &str,
command: DeviceCommand,
) -> Result<Device, AppError> {
send_command_locked_inner(state, device_id, command, false, true).await
}
@@ -14,14 +22,24 @@ async fn send_command_locked_inner(
track_controller_command: bool,
) -> Result<Device, AppError> {
validate_command(&command)?;
let mut device = state.db.get_device(device_id)?
let mut device = state
.db
.get_device(device_id)?
.ok_or_else(|| AppError::NotFound(format!("device {device_id}")))?;
if !device.enabled { return Err(AppError::BadRequest("device is disabled".into())); }
if !device.enabled {
return Err(AppError::BadRequest("device is disabled".into()));
}
// Routine control avoids redundant frames. Explicit safety transitions (global/group OFF,
// detach) may bypass cache de-duplication so stale state cannot leave a unit powered.
let command = if dedupe_against_cache && device.online && device.communication_failures == 0 { command.changed_from(&device) } else { command };
if command.is_empty() { return Ok(device); }
let command = if dedupe_against_cache && device.online && device.communication_failures == 0 {
command.changed_from(&device)
} else {
command
};
if command.is_empty() {
return Ok(device);
}
let controller_command_baseline = device.clone();
let suppress_beep = state.settings.read().await.suppress_device_beep;
let response_started = Instant::now();
@@ -70,20 +88,21 @@ async fn send_command_locked_inner(
Ok(()) => {
device = observed;
let remaining = command.changed_from(&device);
if remaining.is_empty() { Ok(command.clone()) }
else { state.gree.command(&device, &remaining, suppress_beep).await }
}
Err(_) => {
match state.gree.bind(&device).await {
Ok(bound) => {
device.key = Some(bound.key);
device.protocol_version = bound.protocol_version;
state.db.save_device(&device)?;
state.gree.command(&device, &command, suppress_beep).await
}
Err(_) => Err(first_err),
if remaining.is_empty() {
Ok(command.clone())
} else {
state.gree.command(&device, &remaining, suppress_beep).await
}
}
Err(_) => match state.gree.bind(&device).await {
Ok(bound) => {
device.key = Some(bound.key);
device.protocol_version = bound.protocol_version;
state.db.save_device(&device)?;
state.gree.command(&device, &command, suppress_beep).await
}
Err(_) => Err(first_err),
},
};
match retry_result {
Ok(result) => applied_command = result,
@@ -94,8 +113,12 @@ async fn send_command_locked_inner(
}
}
}
if command.quiet.is_some() && applied_command.quiet.is_none() { device.supports_quiet = Some(false); }
if command.sleep.is_some() && applied_command.sleep.is_none() { device.supports_sleep = Some(false); }
if command.quiet.is_some() && applied_command.quiet.is_none() {
device.supports_quiet = Some(false);
}
if command.sleep.is_some() && applied_command.sleep.is_none() {
device.supports_sleep = Some(false);
}
// A command ACK confirms transport/acceptance, but several GREE firmwares keep
// returning the pre-command status for a short settling window. Publishing that first
@@ -105,7 +128,9 @@ async fn send_command_locked_inner(
let verification_delays_ms = [0_u64, 150, 350, 650];
let mut last_verification_error: Option<String> = None;
for delay_ms in verification_delays_ms {
if delay_ms > 0 { sleep(Duration::from_millis(delay_ms)).await; }
if delay_ms > 0 {
sleep(Duration::from_millis(delay_ms)).await;
}
let mut observed = device.clone();
match state.gree.poll(&mut observed).await {
Ok(()) => {
@@ -114,7 +139,9 @@ async fn send_command_locked_inner(
confirmed_state = true;
confirmed_requested_state = requested_matches;
last_verification_error = None;
if requested_matches { break; }
if requested_matches {
break;
}
}
Err(err) => {
last_verification_error = Some(err.to_string());
@@ -125,23 +152,49 @@ async fn send_command_locked_inner(
if confirmed_state && !confirmed_requested_state {
tracing::debug!(device=%device.id, command=?applied_command, "GREE command acknowledged but status still differs after settling window");
} else if !confirmed_state {
let error = last_verification_error.unwrap_or_else(|| "status verification failed".into());
record_poll_failure(&mut device, &format!("command accepted but status verification failed: {error}"));
state.log("warn", "device.command_unconfirmed", &format!("Command accepted by {}, but resulting state could not be verified", device.name), json!({
"device_id": device.id, "error": error
}));
let error =
last_verification_error.unwrap_or_else(|| "status verification failed".into());
record_poll_failure(
&mut device,
&format!("command accepted but status verification failed: {error}"),
);
state.log(
"warn",
"device.command_unconfirmed",
&format!(
"Command accepted by {}, but resulting state could not be verified",
device.name
),
json!({
"device_id": device.id, "error": error
}),
);
}
}
if confirmed_state {
device.response_time_ms = Some(response_started.elapsed().as_millis().min(u64::MAX as u128) as u64);
device.response_time_ms =
Some(response_started.elapsed().as_millis().min(u64::MAX as u128) as u64);
}
state.db.save_device(&device)?;
if !dedupe_against_cache && confirmed_state && !confirmed_requested_state {
if track_controller_command && !command_manual_control_fields(&applied_command).is_empty() {
remember_controller_command(state, device_id, &applied_command, &controller_command_baseline).await;
if track_controller_command
&& !command_manual_control_fields(&applied_command).is_empty()
{
remember_controller_command(
state,
device_id,
&applied_command,
&controller_command_baseline,
)
.await;
}
state.broadcast("device.updated", serde_json::to_value(&device).unwrap_or_default());
return Err(AppError::Device("device did not confirm the requested forced state change".into()));
state.broadcast(
"device.updated",
serde_json::to_value(&device).unwrap_or_default(),
);
return Err(AppError::Device(
"device did not confirm the requested forced state change".into(),
));
}
}
@@ -150,32 +203,54 @@ async fn send_command_locked_inner(
// observed once. Several GREE modules can briefly publish an older snapshot again
// and then return to the controller-requested state. Without this guard that normal
// firmware bounce can be misclassified as a physical/pilot takeover.
remember_controller_command(state, device_id, &applied_command, &controller_command_baseline).await;
remember_controller_command(
state,
device_id,
&applied_command,
&controller_command_baseline,
)
.await;
}
record_device_transition_timestamps(state, &controller_command_baseline, &device)?;
state.log("info", "device.command", &format!("Updated {}", device.name), json!({
"device_id": device.id,
"command": applied_command,
"confirmed": confirmed_state,
}));
state.broadcast("device.updated", serde_json::to_value(&device).unwrap_or_default());
state.log(
"info",
"device.command",
&format!("Updated {}", device.name),
json!({
"device_id": device.id,
"command": applied_command,
"confirmed": confirmed_state,
}),
);
state.broadcast(
"device.updated",
serde_json::to_value(&device).unwrap_or_default(),
);
Ok(device)
}
fn record_device_transition_timestamps(state: &AppState, before: &Device, after: &Device) -> Result<(), AppError> {
fn record_device_transition_timestamps(
state: &AppState,
before: &Device,
after: &Device,
) -> Result<(), AppError> {
let power_changed = before.power != after.power;
let mode_changed = before.mode != after.mode;
if !power_changed && !mode_changed { return Ok(()); }
if !power_changed && !mode_changed {
return Ok(());
}
// This function is often called while the device lock is held, so acquiring a zone lock
// here would invert the global zone -> device order. Merge only these timestamp fields
// with a DB compare-and-swap instead of saving a stale whole-zone snapshot.
for zone in state.db.merge_zone_device_transition_timestamps(
&after.id, power_changed, mode_changed, Utc::now(),
&after.id,
power_changed,
mode_changed,
Utc::now(),
)? {
state.broadcast("zone.updated", serde_json::to_value(&zone)?);
}
Ok(())
}