v0.12.0-preety_code
This commit is contained in:
+175
-51
@@ -1,11 +1,25 @@
|
||||
async fn discover(State(state): State<AppState>, Json(request): Json<DiscoveryRequest>) -> Result<Json<Value>, AppError> {
|
||||
async fn discover(
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<DiscoveryRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let _configuration_guard = state.lock_configuration_operation().await;
|
||||
let settings = state.settings.read().await.clone();
|
||||
let timeout_ms = request.timeout_ms.unwrap_or(settings.discovery_timeout_ms).clamp(500, 30_000);
|
||||
let timeout_ms = request
|
||||
.timeout_ms
|
||||
.unwrap_or(settings.discovery_timeout_ms)
|
||||
.clamp(500, 30_000);
|
||||
let broadcast = request.broadcast.unwrap_or(settings.discovery_broadcast);
|
||||
let protocol_version = request.protocol_version.unwrap_or(0).min(2);
|
||||
let passes = request.passes.unwrap_or(3).clamp(1, 10);
|
||||
let discovered = state.gree.discover(&broadcast, Duration::from_millis(timeout_ms), protocol_version, passes).await
|
||||
let discovered = state
|
||||
.gree
|
||||
.discover(
|
||||
&broadcast,
|
||||
Duration::from_millis(timeout_ms),
|
||||
protocol_version,
|
||||
passes,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| AppError::Device(e.to_string()))?;
|
||||
let mut saved = Vec::new();
|
||||
let mut new_device_ids = Vec::new();
|
||||
@@ -33,31 +47,48 @@ async fn discover(State(state): State<AppState>, Json(request): Json<DiscoveryRe
|
||||
}
|
||||
Err(err) => {
|
||||
merged.last_error = Some(format!("discovered, bind pending: {err}"));
|
||||
state.log("warn", "device.bind_after_discovery", &format!("{}: {err}", merged.name), json!({"device_id": merged.id}));
|
||||
state.log(
|
||||
"warn",
|
||||
"device.bind_after_discovery",
|
||||
&format!("{}: {err}", merged.name),
|
||||
json!({"device_id": merged.id}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
state.db.save_device(&merged)?;
|
||||
if is_new { new_device_ids.push(merged.id.clone()); }
|
||||
if is_new {
|
||||
new_device_ids.push(merged.id.clone());
|
||||
}
|
||||
saved.push(merged);
|
||||
}
|
||||
state.log("info", "discovery.complete", &format!("Discovery found {} device(s)", saved.len()), json!({"count": saved.len(), "protocol_version": protocol_version, "passes": passes, "new_devices": new_device_ids.len()}));
|
||||
state.broadcast("devices.discovered", json!({"devices": saved}));
|
||||
Ok(Json(json!({"count": saved.len(), "devices": saved, "new_device_ids": new_device_ids})))
|
||||
Ok(Json(
|
||||
json!({"count": saved.len(), "devices": saved, "new_device_ids": new_device_ids}),
|
||||
))
|
||||
}
|
||||
|
||||
async fn list_devices(State(state): State<AppState>) -> Result<Json<Vec<Device>>, AppError> {
|
||||
Ok(Json(state.db.list_devices()?))
|
||||
}
|
||||
|
||||
async fn add_device(State(state): State<AppState>, Json(input): Json<ManualDeviceRequest>) -> Result<(StatusCode, Json<Device>), AppError> {
|
||||
async fn add_device(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<ManualDeviceRequest>,
|
||||
) -> Result<(StatusCode, Json<Device>), AppError> {
|
||||
let _configuration_guard = state.lock_configuration_operation().await;
|
||||
if input.name.trim().is_empty() || input.mac.trim().is_empty() || input.ip.trim().is_empty() {
|
||||
return Err(AppError::BadRequest("name, mac and ip are required".into()));
|
||||
}
|
||||
input.ip.parse::<IpAddr>().map_err(|_| AppError::BadRequest("invalid IP address".into()))?;
|
||||
input
|
||||
.ip
|
||||
.parse::<IpAddr>()
|
||||
.map_err(|_| AppError::BadRequest("invalid IP address".into()))?;
|
||||
if state.db.get_device_by_mac(&input.mac)?.is_some() {
|
||||
return Err(AppError::BadRequest("a device with this MAC already exists".into()));
|
||||
return Err(AppError::BadRequest(
|
||||
"a device with this MAC already exists".into(),
|
||||
));
|
||||
}
|
||||
let now = Utc::now();
|
||||
let normalized_mac = input.mac.replace([':', '-'], "").to_ascii_uppercase();
|
||||
@@ -106,25 +137,54 @@ async fn add_device(State(state): State<AppState>, Json(input): Json<ManualDevic
|
||||
updated_at: now,
|
||||
};
|
||||
state.db.save_device(&device)?;
|
||||
state.log("info", "device.created", &format!("Added {}", device.name), json!({"device_id": device.id}));
|
||||
state.log(
|
||||
"info",
|
||||
"device.created",
|
||||
&format!("Added {}", device.name),
|
||||
json!({"device_id": device.id}),
|
||||
);
|
||||
state.broadcast("device.created", serde_json::to_value(&device)?);
|
||||
Ok((StatusCode::CREATED, Json(device)))
|
||||
}
|
||||
|
||||
async fn get_device(State(state): State<AppState>, Path(id): Path<String>) -> Result<Json<Device>, AppError> {
|
||||
state.db.get_device(&id)?.map(Json).ok_or_else(|| AppError::NotFound(format!("device {id}")))
|
||||
async fn get_device(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<Device>, AppError> {
|
||||
state
|
||||
.db
|
||||
.get_device(&id)?
|
||||
.map(Json)
|
||||
.ok_or_else(|| AppError::NotFound(format!("device {id}")))
|
||||
}
|
||||
|
||||
async fn patch_device(State(state): State<AppState>, Path(id): Path<String>, Json(patch): Json<DevicePatch>) -> Result<Json<Device>, AppError> {
|
||||
async fn patch_device(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(patch): Json<DevicePatch>,
|
||||
) -> Result<Json<Device>, AppError> {
|
||||
let _configuration_guard = state.lock_configuration_operation().await;
|
||||
if patch.enabled == Some(false) {
|
||||
engine::disable_device_safely(&state, &id).await?;
|
||||
}
|
||||
let _device_guard = state.lock_device_operation(&id).await;
|
||||
let mut device = state.db.get_device(&id)?.ok_or_else(|| AppError::NotFound(format!("device {id}")))?;
|
||||
if let Some(v) = patch.name { if !v.trim().is_empty() { device.name = v.trim().to_string(); } }
|
||||
if let Some(v) = patch.ip { v.parse::<IpAddr>().map_err(|_| AppError::BadRequest("invalid IP address".into()))?; device.ip = v; }
|
||||
if let Some(v) = patch.port { device.port = v; }
|
||||
let mut device = state
|
||||
.db
|
||||
.get_device(&id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device {id}")))?;
|
||||
if let Some(v) = patch.name {
|
||||
if !v.trim().is_empty() {
|
||||
device.name = v.trim().to_string();
|
||||
}
|
||||
}
|
||||
if let Some(v) = patch.ip {
|
||||
v.parse::<IpAddr>()
|
||||
.map_err(|_| AppError::BadRequest("invalid IP address".into()))?;
|
||||
device.ip = v;
|
||||
}
|
||||
if let Some(v) = patch.port {
|
||||
device.port = v;
|
||||
}
|
||||
if let Some(v) = patch.protocol_version {
|
||||
let v = v.min(2);
|
||||
if device.protocol_version != v {
|
||||
@@ -139,8 +199,12 @@ async fn patch_device(State(state): State<AppState>, Path(id): Path<String>, Jso
|
||||
device.supports_sleep = None;
|
||||
}
|
||||
}
|
||||
if let Some(v) = patch.key { device.key = v.filter(|x| !x.trim().is_empty()); }
|
||||
if let Some(v) = patch.enabled { device.enabled = v; }
|
||||
if let Some(v) = patch.key {
|
||||
device.key = v.filter(|x| !x.trim().is_empty());
|
||||
}
|
||||
if let Some(v) = patch.enabled {
|
||||
device.enabled = v;
|
||||
}
|
||||
device.updated_at = Utc::now();
|
||||
state.db.save_device(&device)?;
|
||||
state.broadcast("device.updated", serde_json::to_value(&device)?);
|
||||
@@ -151,7 +215,10 @@ async fn patch_device(State(state): State<AppState>, Path(id): Path<String>, Jso
|
||||
Ok(Json(device))
|
||||
}
|
||||
|
||||
async fn delete_device(State(state): State<AppState>, Path(id): Path<String>) -> Result<StatusCode, AppError> {
|
||||
async fn delete_device(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<StatusCode, AppError> {
|
||||
let _configuration_guard = state.lock_configuration_operation().await;
|
||||
// Keep reference validation and the destructive DB operation in one serialized window.
|
||||
// Lock order for cross-resource destructive operations: configuration -> automation -> house -> schedule -> cycle -> zones -> device.
|
||||
@@ -159,14 +226,21 @@ async fn delete_device(State(state): State<AppState>, Path(id): Path<String>) ->
|
||||
let _house_guard = state.lock_house_operation().await;
|
||||
let _schedule_guard = state.lock_schedule_operation().await;
|
||||
let _cycle_guard = state.lock_zone_control_cycle().await;
|
||||
if state.db.get_device(&id)?.is_none() { return Err(AppError::NotFound(format!("device {id}"))); }
|
||||
if state.db.get_device(&id)?.is_none() {
|
||||
return Err(AppError::NotFound(format!("device {id}")));
|
||||
}
|
||||
if state.db.list_automations()?.iter().any(|item| {
|
||||
item.trigger_device_id.as_deref() == Some(id.as_str())
|
||||
|| (item.action_group_id.is_none() && item.action_device_id == id)
|
||||
}) {
|
||||
return Err(AppError::BadRequest("device is used by an automation; remove or retarget that automation first".into()));
|
||||
return Err(AppError::BadRequest(
|
||||
"device is used by an automation; remove or retarget that automation first".into(),
|
||||
));
|
||||
}
|
||||
let removed_zone_ids: std::collections::HashSet<String> = state.db.list_zones()?.into_iter()
|
||||
let removed_zone_ids: std::collections::HashSet<String> = state
|
||||
.db
|
||||
.list_zones()?
|
||||
.into_iter()
|
||||
.filter(|zone| zone.device_id == id)
|
||||
.map(|zone| zone.id)
|
||||
.collect();
|
||||
@@ -178,20 +252,39 @@ async fn delete_device(State(state): State<AppState>, Path(id): Path<String>) ->
|
||||
}
|
||||
ensure_zone_removal_safe(&state, &removed_zone_ids)?;
|
||||
ensure_device_stopped_for_detach(&state, &id, "device.deleted").await?;
|
||||
if !state.db.delete_device(&id)? { return Err(AppError::NotFound(format!("device {id}"))); }
|
||||
if !state.db.delete_device(&id)? {
|
||||
return Err(AppError::NotFound(format!("device {id}")));
|
||||
}
|
||||
drop(zone_guards);
|
||||
remove_zone_ids_from_groups_locked(&state, &removed_zone_ids).await?;
|
||||
state.log("info", "device.deleted", "Device deleted", json!({"device_id": id}));
|
||||
state.log(
|
||||
"info",
|
||||
"device.deleted",
|
||||
"Device deleted",
|
||||
json!({"device_id": id}),
|
||||
);
|
||||
state.broadcast("device.deleted", json!({"id": id}));
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
async fn bind_device(State(state): State<AppState>, Path(id): Path<String>) -> Result<Json<Device>, AppError> {
|
||||
async fn bind_device(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<Device>, AppError> {
|
||||
let _configuration_guard = state.lock_configuration_operation().await;
|
||||
let _device_guard = state.lock_device_operation(&id).await;
|
||||
let mut device = state.db.get_device(&id)?.ok_or_else(|| AppError::NotFound(format!("device {id}")))?;
|
||||
if device.simulated { return Ok(Json(device)); }
|
||||
let bound = state.gree.bind(&device).await.map_err(|e| AppError::Device(e.to_string()))?;
|
||||
let mut device = state
|
||||
.db
|
||||
.get_device(&id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device {id}")))?;
|
||||
if device.simulated {
|
||||
return Ok(Json(device));
|
||||
}
|
||||
let bound = state
|
||||
.gree
|
||||
.bind(&device)
|
||||
.await
|
||||
.map_err(|e| AppError::Device(e.to_string()))?;
|
||||
device.key = Some(bound.key);
|
||||
device.protocol_version = bound.protocol_version;
|
||||
device.communication_failures = 0;
|
||||
@@ -200,17 +293,35 @@ async fn bind_device(State(state): State<AppState>, Path(id): Path<String>) -> R
|
||||
device.last_error = None;
|
||||
device.updated_at = Utc::now();
|
||||
state.db.save_device(&device)?;
|
||||
state.log("info", "device.bound", &format!("Bound {}", device.name), json!({"device_id": id}));
|
||||
state.log(
|
||||
"info",
|
||||
"device.bound",
|
||||
&format!("Bound {}", device.name),
|
||||
json!({"device_id": id}),
|
||||
);
|
||||
Ok(Json(device))
|
||||
}
|
||||
|
||||
async fn poll_device(State(state): State<AppState>, Path(id): Path<String>) -> Result<Json<Device>, AppError> {
|
||||
async fn poll_device(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<Device>, AppError> {
|
||||
Ok(Json(engine::poll_one(&state, &id).await?))
|
||||
}
|
||||
|
||||
async fn probe_device(State(state): State<AppState>, Path(id): Path<String>) -> Result<Json<Value>, AppError> {
|
||||
let device = state.db.get_device(&id)?.ok_or_else(|| AppError::NotFound(format!("device {id}")))?;
|
||||
let response_time_ms = state.gree.probe(&device).await.map_err(|err| AppError::Device(err.to_string()))?;
|
||||
async fn probe_device(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let device = state
|
||||
.db
|
||||
.get_device(&id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device {id}")))?;
|
||||
let response_time_ms = state
|
||||
.gree
|
||||
.probe(&device)
|
||||
.await
|
||||
.map_err(|err| AppError::Device(err.to_string()))?;
|
||||
Ok(Json(json!({
|
||||
"device_id": device.id,
|
||||
"response_time_ms": response_time_ms,
|
||||
@@ -226,23 +337,36 @@ struct ManualDeviceCommandRequest {
|
||||
manual_override: bool,
|
||||
}
|
||||
|
||||
async fn command_device(State(state): State<AppState>, Path(id): Path<String>, Json(request): Json<ManualDeviceCommandRequest>) -> Result<Json<Device>, AppError> {
|
||||
Ok(Json(engine::send_manual_command(
|
||||
&state,
|
||||
&id,
|
||||
request.command,
|
||||
"device.manual_control",
|
||||
request.manual_override,
|
||||
).await?))
|
||||
async fn command_device(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(request): Json<ManualDeviceCommandRequest>,
|
||||
) -> Result<Json<Device>, AppError> {
|
||||
Ok(Json(
|
||||
engine::send_manual_command(
|
||||
&state,
|
||||
&id,
|
||||
request.command,
|
||||
"device.manual_control",
|
||||
request.manual_override,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
async fn command_home_assistant_device(State(state): State<AppState>, Path(id): Path<String>, Json(command): Json<DeviceCommand>) -> Result<Json<Device>, AppError> {
|
||||
Ok(Json(engine::send_manual_command(
|
||||
&state,
|
||||
&id,
|
||||
command,
|
||||
"home_assistant.device_manual_control",
|
||||
false,
|
||||
).await?))
|
||||
async fn command_home_assistant_device(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(command): Json<DeviceCommand>,
|
||||
) -> Result<Json<Device>, AppError> {
|
||||
Ok(Json(
|
||||
engine::send_manual_command(
|
||||
&state,
|
||||
&id,
|
||||
command,
|
||||
"home_assistant.device_manual_control",
|
||||
false,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user