v0.14.0
This commit is contained in:
+102
-5
@@ -17,12 +17,16 @@ async fn export_configuration(
|
||||
let settings = state.settings.read().await.clone();
|
||||
let mut export = state.db.export_configuration(settings)?;
|
||||
sanitize_configuration_runtime(&mut export);
|
||||
// Cloud credentials are account-scoped secrets and must never be returned to the frontend,
|
||||
// including configuration exports. Import can reuse the already configured password on the
|
||||
// target controller when account/region match.
|
||||
export.settings.gree_cloud.password.clear();
|
||||
Ok(Json(export))
|
||||
}
|
||||
|
||||
fn validate_configuration_header(export: &ConfigurationExport) -> Result<(), AppError> {
|
||||
if export.format_version != 3 {
|
||||
return Err(AppError::BadRequest("unsupported configuration export version; version 3 is required by GREE Controller 0.12.x".into()));
|
||||
return Err(AppError::BadRequest("unsupported configuration export version; version 3 is required by GREE Controller 0.14.0".into()));
|
||||
}
|
||||
if export.settings.control_strategy != "setpoint" {
|
||||
return Err(AppError::BadRequest(
|
||||
@@ -108,14 +112,16 @@ fn validate_configuration_devices_and_zones(
|
||||
export: &ConfigurationExport,
|
||||
ids: &ConfigurationIds<'_>,
|
||||
) -> Result<(), AppError> {
|
||||
let device_macs: std::collections::HashSet<&str> = export
|
||||
// The same physical unit may intentionally exist once as Local and once as GREE Cloud.
|
||||
// Reject duplicates only within the same explicit transport.
|
||||
let device_transport_macs: std::collections::HashSet<(ConnectionType, &str)> = export
|
||||
.devices
|
||||
.iter()
|
||||
.map(|item| item.mac.as_str())
|
||||
.map(|item| (item.connection_type, item.mac.as_str()))
|
||||
.collect();
|
||||
if device_macs.len() != export.devices.len() {
|
||||
if device_transport_macs.len() != export.devices.len() {
|
||||
return Err(AppError::BadRequest(
|
||||
"import contains duplicate device MAC addresses".into(),
|
||||
"import contains duplicate device MAC addresses for the same connection type".into(),
|
||||
));
|
||||
}
|
||||
if export
|
||||
@@ -557,6 +563,15 @@ fn normalize_imported_runtime_settings(settings: &mut RuntimeSettings) -> Result
|
||||
settings.compressor_protection_enabled = gree.compressor_protection_enabled;
|
||||
settings.compressor_protection_seconds = gree.compressor_protection_seconds;
|
||||
|
||||
if crate::protocol::gree_cloud::region_base_url(settings.gree_cloud.region.trim()).is_none() {
|
||||
return Err(AppError::BadRequest("unsupported GREE Cloud region".into()));
|
||||
}
|
||||
settings.gree_cloud.polling_interval_seconds =
|
||||
settings.gree_cloud.polling_interval_seconds.clamp(30, 3600);
|
||||
if settings.gree_cloud.account_id.trim().is_empty() {
|
||||
settings.gree_cloud.account_id = "default".into();
|
||||
}
|
||||
|
||||
settings.history_retention_days = settings.history_retention_days.clamp(1, 3650);
|
||||
settings.event_log_retention_days = settings.event_log_retention_days.clamp(1, 3650);
|
||||
settings.influxdb.history_threshold_days =
|
||||
@@ -601,6 +616,81 @@ fn prepare_configuration_import(export: &mut ConfigurationExport) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn hydrate_imported_cloud_device_keys(
|
||||
state: &AppState,
|
||||
export: &mut ConfigurationExport,
|
||||
) -> Result<(), AppError> {
|
||||
if export.settings.gree_cloud.password.trim().is_empty() {
|
||||
let current = state.settings.read().await.gree_cloud.clone();
|
||||
if current.region.eq_ignore_ascii_case(&export.settings.gree_cloud.region)
|
||||
&& current.username.eq_ignore_ascii_case(&export.settings.gree_cloud.username)
|
||||
&& !current.password.trim().is_empty()
|
||||
{
|
||||
export.settings.gree_cloud.password = current.password;
|
||||
}
|
||||
}
|
||||
let needs_cloud_keys = export.devices.iter().any(|device| {
|
||||
device.connection_type == ConnectionType::GreeCloud
|
||||
&& device.key.as_deref().unwrap_or_default().is_empty()
|
||||
});
|
||||
if !needs_cloud_keys {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let settings = &export.settings.gree_cloud;
|
||||
let mut api = crate::protocol::gree_cloud::GreeCloudApi::for_region(
|
||||
state.http.clone(),
|
||||
&settings.region,
|
||||
&settings.username,
|
||||
&settings.password,
|
||||
)
|
||||
.map_err(|err| AppError::BadRequest(format!(
|
||||
"cannot restore GREE Cloud device secrets from account: {err}"
|
||||
)))?;
|
||||
api.login().await.map_err(|err| {
|
||||
AppError::BadRequest(format!(
|
||||
"cannot restore GREE Cloud device secrets: account login failed: {err}"
|
||||
))
|
||||
})?;
|
||||
let discovered = api.get_all_devices().await.map_err(|err| {
|
||||
AppError::BadRequest(format!(
|
||||
"cannot restore GREE Cloud device secrets: discovery failed: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
for device in export
|
||||
.devices
|
||||
.iter_mut()
|
||||
.filter(|device| device.connection_type == ConnectionType::GreeCloud)
|
||||
{
|
||||
if device.key.as_deref().is_some_and(|key| !key.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
let cloud_id = device.cloud_device_id.as_deref().unwrap_or(&device.mac);
|
||||
let Some(found) = discovered
|
||||
.iter()
|
||||
.find(|candidate| candidate.mac.eq_ignore_ascii_case(cloud_id))
|
||||
else {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"cannot restore GREE Cloud device {}: it is not present in the configured account",
|
||||
device.name
|
||||
)));
|
||||
};
|
||||
device.key = Some(found.key.clone());
|
||||
let normalized_cloud_mac = found.mac.replace([':', '-'], "").to_ascii_uppercase();
|
||||
device.cloud_device_id = Some(normalized_cloud_mac.clone());
|
||||
device.cloud_parent_mac = Some(crate::protocol::gree_cloud::parent_mac(&normalized_cloud_mac));
|
||||
device.cloud_account_id = Some(settings.account_id.clone());
|
||||
if device.model.trim().is_empty() {
|
||||
device.model = found.model.clone().unwrap_or_default();
|
||||
}
|
||||
if device.firmware.trim().is_empty() {
|
||||
device.firmware = found.version.clone().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn lock_configuration_resources(
|
||||
state: &AppState,
|
||||
current_zones: &[Zone],
|
||||
@@ -720,6 +810,7 @@ async fn import_configuration(
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
validate_configuration_export(&export)?;
|
||||
prepare_configuration_import(&mut export)?;
|
||||
hydrate_imported_cloud_device_keys(&state, &mut export).await?;
|
||||
|
||||
let _configuration_guard = state.lock_configuration_operation().await;
|
||||
let _automation_guard = state.lock_automation_operation().await;
|
||||
@@ -740,6 +831,12 @@ async fn import_configuration(
|
||||
state
|
||||
.debug_gree_frames
|
||||
.store(export.settings.debug.gree_frames, Ordering::Relaxed);
|
||||
state
|
||||
.debug_cloud_requests
|
||||
.store(export.settings.debug.cloud_requests, Ordering::Relaxed);
|
||||
state
|
||||
.debug_cloud_mqtt
|
||||
.store(export.settings.debug.cloud_mqtt, Ordering::Relaxed);
|
||||
*state.settings.write().await = export.settings.clone();
|
||||
reconcile_imported_devices(&state, &export).await?;
|
||||
state
|
||||
|
||||
Reference in New Issue
Block a user