This commit is contained in:
Mateusz Gruszczyński
2026-09-17 08:52:02 +02:00
parent ff4f2b60e7
commit b7846c3b9f
87 changed files with 3783 additions and 1418 deletions
+68 -20
View File
@@ -50,7 +50,11 @@ fn collect_configuration_ids(
let ids = ConfigurationIds {
devices: export.devices.iter().map(|item| item.id.as_str()).collect(),
zones: export.zones.iter().map(|item| item.id.as_str()).collect(),
device_groups: export.device_groups.iter().map(|item| item.id.as_str()).collect(),
device_groups: export
.device_groups
.iter()
.map(|item| item.id.as_str())
.collect(),
schedules: export
.schedules
.iter()
@@ -190,21 +194,33 @@ fn validate_configuration_devices_and_zones(
}
if !installation_devices.insert(device_id.as_str()) {
return Err(AppError::BadRequest(
"import assigns one device to more than one split/multisplit installation".into(),
"import assigns one device to more than one split/multisplit installation"
.into(),
));
}
}
if group.energy_device_id.as_deref().is_some_and(|id| !group.device_ids.iter().any(|member| member == id)) {
if group
.energy_device_id
.as_deref()
.is_some_and(|id| !group.device_ids.iter().any(|member| member == id))
{
return Err(AppError::BadRequest(
"import contains an installation energy source outside the installation".into(),
));
}
if group.energy_source == EnergySourcePreference::GreeCloud && group.energy_device_id.is_none() {
if group.energy_source == EnergySourcePreference::GreeCloud
&& group.energy_device_id.is_none()
{
return Err(AppError::BadRequest(
"import contains a GREE Cloud installation without an energy source device".into(),
));
}
if group.energy_source == EnergySourcePreference::HomeAssistant && group.ha_energy_entity_id.as_deref().map_or(true, |value| value.trim().is_empty()) {
if group.energy_source == EnergySourcePreference::HomeAssistant
&& group
.ha_energy_entity_id
.as_deref()
.map_or(true, |value| value.trim().is_empty())
{
return Err(AppError::BadRequest(
"import contains a Home Assistant installation without an energy entity".into(),
));
@@ -213,23 +229,43 @@ fn validate_configuration_devices_and_zones(
let energy_device = export.devices.iter().find(|device| device.id == energy_device_id).ok_or_else(|| {
AppError::BadRequest("import contains an installation energy source referencing a missing device".into())
})?;
if energy_device.connection_type != ConnectionType::GreeCloud || !energy_device.capabilities.energy_meter {
if energy_device.connection_type != ConnectionType::GreeCloud
|| !energy_device.capabilities.energy_meter
{
return Err(AppError::BadRequest(
"import contains an installation energy source without a GREE Cloud energy meter".into(),
));
}
}
if group.ha_energy_entity_id.as_deref().is_some_and(|value| !value.trim().is_empty()) {
if group
.ha_energy_entity_id
.as_deref()
.is_some_and(|value| !value.trim().is_empty())
{
if group.ha_energy_device_class.as_deref() != Some("energy")
|| !matches!(group.ha_energy_state_class.as_deref(), Some("total" | "total_increasing"))
|| !matches!(group.ha_energy_unit.as_deref().map(str::to_ascii_lowercase).as_deref(), Some("wh" | "kwh"))
|| !matches!(
group.ha_energy_state_class.as_deref(),
Some("total" | "total_increasing")
)
|| !matches!(
group
.ha_energy_unit
.as_deref()
.map(str::to_ascii_lowercase)
.as_deref(),
Some("wh" | "kwh")
)
{
return Err(AppError::BadRequest(
"import contains an invalid Home Assistant cumulative energy entity".into(),
));
}
}
if group.outdoor_temperature_device_id.as_deref().is_some_and(|id| !group.device_ids.iter().any(|member| member == id)) {
if group
.outdoor_temperature_device_id
.as_deref()
.is_some_and(|id| !group.device_ids.iter().any(|member| member == id))
{
return Err(AppError::BadRequest(
"import contains an installation outdoor-temperature source outside the installation".into(),
));
@@ -652,10 +688,14 @@ fn normalize_imported_runtime_settings(settings: &mut RuntimeSettings) -> Result
}
settings.gree_cloud.polling_interval_seconds =
settings.gree_cloud.polling_interval_seconds.clamp(30, 3600);
settings.gree_cloud.connectivity_metrics_interval_seconds =
settings.gree_cloud.connectivity_metrics_interval_seconds.clamp(30, 3600);
settings.gree_cloud.connectivity_metrics_sample_count =
settings.gree_cloud.connectivity_metrics_sample_count.clamp(1, 10);
settings.gree_cloud.connectivity_metrics_interval_seconds = settings
.gree_cloud
.connectivity_metrics_interval_seconds
.clamp(30, 3600);
settings.gree_cloud.connectivity_metrics_sample_count = settings
.gree_cloud
.connectivity_metrics_sample_count
.clamp(1, 10);
if settings.gree_cloud.account_id.trim().is_empty() {
settings.gree_cloud.account_id = "default".into();
}
@@ -710,8 +750,12 @@ async fn hydrate_imported_cloud_device_keys(
) -> 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)
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;
@@ -732,9 +776,11 @@ async fn hydrate_imported_cloud_device_keys(
&settings.username,
&settings.password,
)
.map_err(|err| AppError::BadRequest(format!(
"cannot restore GREE Cloud device secrets from account: {err}"
)))?;
.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}"
@@ -767,7 +813,9 @@ async fn hydrate_imported_cloud_device_keys(
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_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();