v0.3.5
This commit is contained in:
+56
-19
@@ -66,23 +66,39 @@ pub async fn send_command(state: &AppState, device_id: &str, command: DeviceComm
|
||||
} else {
|
||||
if device.key.as_deref().unwrap_or_default().is_empty() {
|
||||
match state.gree.bind(&device).await {
|
||||
Ok(key) => {
|
||||
device.key = Some(key);
|
||||
Ok(bound) => {
|
||||
device.key = Some(bound.key);
|
||||
device.protocol_version = bound.protocol_version;
|
||||
device.communication_failures = 0;
|
||||
state.db.save_device(&device)?;
|
||||
state.log("info", "device.bound", &format!("Bound {}", device.name), json!({"device_id": device.id}));
|
||||
state.log("info", "device.bound", &format!("Bound {} using protocol V{}", device.name, device.protocol_version), json!({"device_id": device.id, "protocol_version": device.protocol_version}));
|
||||
}
|
||||
Err(err) => {
|
||||
mark_device_error(state, &mut device, &err.to_string())?;
|
||||
register_device_failure(state, &mut device, &err.to_string())?;
|
||||
return Err(AppError::Device(err.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(err) = state.gree.command(&device, &command).await {
|
||||
mark_device_error(state, &mut device, &err.to_string())?;
|
||||
return Err(AppError::Device(err.to_string()));
|
||||
if let Err(first_err) = state.gree.command(&device, &command).await {
|
||||
// Retry once after a fresh bind. This covers stale keys and devices that
|
||||
// switched between ECB/GCM after a firmware update.
|
||||
let retry_result = 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).await
|
||||
}
|
||||
Err(_) => Err(first_err),
|
||||
};
|
||||
if let Err(err) = retry_result {
|
||||
register_device_failure(state, &mut device, &err.to_string())?;
|
||||
return Err(AppError::Device(err.to_string()));
|
||||
}
|
||||
}
|
||||
command.apply(&mut device);
|
||||
device.online = true;
|
||||
device.communication_failures = 0;
|
||||
device.last_seen = Some(Utc::now());
|
||||
device.last_error = None;
|
||||
state.db.save_device(&device)?;
|
||||
@@ -124,19 +140,30 @@ async fn poll_device(state: &AppState, device: &mut Device) {
|
||||
}
|
||||
if device.key.as_deref().unwrap_or_default().is_empty() {
|
||||
match state.gree.bind(device).await {
|
||||
Ok(key) => device.key = Some(key),
|
||||
Ok(bound) => {
|
||||
device.key = Some(bound.key);
|
||||
device.protocol_version = bound.protocol_version;
|
||||
device.communication_failures = 0;
|
||||
}
|
||||
Err(err) => {
|
||||
device.online = false;
|
||||
device.last_error = Some(err.to_string());
|
||||
device.updated_at = Utc::now();
|
||||
record_poll_failure(device, &err.to_string());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(err) = state.gree.poll(device).await {
|
||||
device.online = false;
|
||||
device.last_error = Some(err.to_string());
|
||||
device.updated_at = Utc::now();
|
||||
if let Err(first_err) = state.gree.poll(device).await {
|
||||
// A stale key or wrong cipher should heal automatically during polling.
|
||||
// Rebind once, then retry the status request before counting a failure.
|
||||
match state.gree.bind(device).await {
|
||||
Ok(bound) => {
|
||||
device.key = Some(bound.key);
|
||||
device.protocol_version = bound.protocol_version;
|
||||
if let Err(err) = state.gree.poll(device).await {
|
||||
record_poll_failure(device, &err.to_string());
|
||||
}
|
||||
}
|
||||
Err(_) => record_poll_failure(device, &first_err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,18 +211,28 @@ fn record_reading(state: &AppState, device: &Device) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mark_device_error(state: &AppState, device: &mut Device, error: &str) -> Result<(), AppError> {
|
||||
device.online = false;
|
||||
fn record_poll_failure(device: &mut Device, error: &str) {
|
||||
device.communication_failures = device.communication_failures.saturating_add(1);
|
||||
// A single dropped UDP response is not enough to declare an AC offline.
|
||||
if device.communication_failures >= 3 { device.online = false; }
|
||||
device.last_error = Some(error.to_string());
|
||||
device.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
fn register_device_failure(state: &AppState, device: &mut Device, error: &str) -> Result<(), AppError> {
|
||||
record_poll_failure(device, error);
|
||||
state.db.save_device(device)?;
|
||||
state.log("error", "device.error", &format!("{}: {error}", device.name), json!({"device_id": device.id}));
|
||||
state.log("warn", "device.communication_error", &format!("{}: {error}", device.name), json!({
|
||||
"device_id": device.id,
|
||||
"consecutive_failures": device.communication_failures,
|
||||
"offline": !device.online,
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_command(command: &DeviceCommand) -> Result<(), AppError> {
|
||||
if let Some(value) = command.target_temperature {
|
||||
if !(8.0..=32.0).contains(&value) { return Err(AppError::BadRequest("target temperature must be between 8 and 32 C".into())); }
|
||||
if !(8.0..=30.0).contains(&value) { return Err(AppError::BadRequest("target temperature must be between 8 and 30 C".into())); }
|
||||
}
|
||||
if let Some(value) = command.fan_speed {
|
||||
if value > 5 { return Err(AppError::BadRequest("fan speed must be between 0 and 5".into())); }
|
||||
|
||||
Reference in New Issue
Block a user