v0.14.21
This commit is contained in:
+244
-152
@@ -133,11 +133,7 @@ fn sensor_history_with_fallback(
|
||||
if !matches!(zone.sensor_source.as_str(), "home_assistant" | "combined") {
|
||||
continue;
|
||||
}
|
||||
let entity_id = zone
|
||||
.ha_entity_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or("");
|
||||
let entity_id = zone.ha_entity_id.as_deref().map(str::trim).unwrap_or("");
|
||||
if entity_id.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -177,9 +173,10 @@ fn sensor_history_with_fallback(
|
||||
if entity_id.is_empty() || existing.contains(entity_id) {
|
||||
continue;
|
||||
}
|
||||
let rows = state
|
||||
.db
|
||||
.list_zone_history(Some(&zone.id), since.clone(), bucket_seconds, limit)?;
|
||||
let rows =
|
||||
state
|
||||
.db
|
||||
.list_zone_history(Some(&zone.id), since.clone(), bucket_seconds, limit)?;
|
||||
let mut added = false;
|
||||
for row in rows {
|
||||
if let Some(temperature) = row.outdoor_temperature {
|
||||
@@ -342,13 +339,7 @@ async fn combined_sensor_history(
|
||||
.db
|
||||
.list_ha_history(entity_id, start, bucket_seconds, limit)?)
|
||||
} else {
|
||||
sensor_history_with_fallback(
|
||||
state,
|
||||
start,
|
||||
bucket_seconds,
|
||||
limit,
|
||||
outdoor_entity,
|
||||
)
|
||||
sensor_history_with_fallback(state, start, bucket_seconds, limit, outdoor_entity)
|
||||
}
|
||||
};
|
||||
if !influx.enabled || since >= cutoff {
|
||||
@@ -526,6 +517,7 @@ struct PublicChartPoint {
|
||||
struct PublicChartSeries {
|
||||
key: String,
|
||||
label: String,
|
||||
label_key: &'static str,
|
||||
dashed: bool,
|
||||
points: Vec<PublicChartPoint>,
|
||||
}
|
||||
@@ -537,7 +529,9 @@ fn validate_public_chart_spec(series: &[String]) -> Result<(), AppError> {
|
||||
.iter()
|
||||
.any(|item| item.is_empty() || item.len() > 256)
|
||||
{
|
||||
return Err(AppError::BadRequest("invalid custom chart definition".into()));
|
||||
return Err(AppError::BadRequest(
|
||||
"invalid custom chart definition".into(),
|
||||
));
|
||||
}
|
||||
|
||||
for key in series {
|
||||
@@ -545,7 +539,7 @@ fn validate_public_chart_spec(series: &[String]) -> Result<(), AppError> {
|
||||
let kind = parts.next().unwrap_or_default();
|
||||
let id = parts.next().unwrap_or_default();
|
||||
let field = parts.next().unwrap_or_default();
|
||||
if id.trim().is_empty() || public_chart_field_label(kind, field, "en").is_none() {
|
||||
if id.trim().is_empty() || public_chart_field_label_key(kind, field).is_none() {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"unsupported custom chart series: {key}"
|
||||
)));
|
||||
@@ -568,22 +562,17 @@ async fn create_public_custom_chart(
|
||||
validate_public_chart_spec(&input.series)?;
|
||||
|
||||
let hours = input.hours.unwrap_or(24).clamp(1, 24 * 3650);
|
||||
let lang = if input.lang.as_deref() == Some("pl") {
|
||||
"pl"
|
||||
} else {
|
||||
"en"
|
||||
};
|
||||
let default_title = if lang == "pl" {
|
||||
"Wykres niestandardowy"
|
||||
} else {
|
||||
"Custom chart"
|
||||
};
|
||||
let requested_lang = input.lang.as_deref().unwrap_or_default().trim();
|
||||
let lang = LANGUAGE_ASSETS
|
||||
.iter()
|
||||
.find(|(code, _)| *code == requested_lang)
|
||||
.map(|(code, _)| (*code).to_string())
|
||||
.unwrap_or_else(|| DEFAULT_LANGUAGE_CODE.to_string());
|
||||
let title = input
|
||||
.title
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(default_title)
|
||||
.unwrap_or_default()
|
||||
.chars()
|
||||
.take(120)
|
||||
.collect::<String>();
|
||||
@@ -591,7 +580,7 @@ async fn create_public_custom_chart(
|
||||
title,
|
||||
series: input.series,
|
||||
hours,
|
||||
lang: lang.to_string(),
|
||||
lang,
|
||||
};
|
||||
|
||||
let token = generate_public_chart_token();
|
||||
@@ -605,20 +594,19 @@ async fn create_public_custom_chart(
|
||||
})))
|
||||
}
|
||||
|
||||
fn public_chart_field_label(kind: &str, field: &str, lang: &str) -> Option<&'static str> {
|
||||
let pl = lang == "pl";
|
||||
fn public_chart_field_label_key(kind: &str, field: &str) -> Option<&'static str> {
|
||||
match (kind, field) {
|
||||
("device", "indoor") => Some(if pl { "Temperatura wewnętrzna" } else { "Indoor temperature" }),
|
||||
("device", "outdoor") => Some(if pl { "Temperatura zewnętrzna GREE" } else { "GREE outdoor temperature" }),
|
||||
("device", "target") => Some(if pl { "Temperatura zadana urządzenia" } else { "Device target" }),
|
||||
("installation", "outdoor") => Some(if pl { "Wspólna temperatura zewnętrzna" } else { "Shared outdoor temperature" }),
|
||||
("zone", "control") => Some(if pl { "Temperatura sterująca" } else { "Control temperature" }),
|
||||
("zone", "gree") => Some(if pl { "Czujnik GREE" } else { "GREE sensor" }),
|
||||
("zone", "external") => Some(if pl { "Czujnik pomieszczenia" } else { "Room sensor" }),
|
||||
("zone", "target") => Some(if pl { "Temperatura docelowa" } else { "Comfort target" }),
|
||||
("zone", "device_target") => Some(if pl { "Nastawa urządzenia" } else { "Device setpoint" }),
|
||||
("zone", "outdoor") => Some(if pl { "Temperatura zewnętrzna" } else { "Outdoor temperature" }),
|
||||
("ha", "temperature") => Some(if pl { "Temperatura" } else { "Temperature" }),
|
||||
("device", "indoor") => Some("publicChart.field.deviceIndoor"),
|
||||
("device", "outdoor") => Some("publicChart.field.deviceOutdoor"),
|
||||
("device", "target") => Some("publicChart.field.deviceTarget"),
|
||||
("installation", "outdoor") => Some("publicChart.field.sharedOutdoor"),
|
||||
("zone", "control") => Some("publicChart.field.zoneControl"),
|
||||
("zone", "gree") => Some("publicChart.field.greeSensor"),
|
||||
("zone", "external") => Some("publicChart.field.roomSensor"),
|
||||
("zone", "target") => Some("publicChart.field.comfortTarget"),
|
||||
("zone", "device_target") => Some("publicChart.field.deviceSetpoint"),
|
||||
("zone", "outdoor") => Some("publicChart.field.outdoor"),
|
||||
("ha", "temperature") => Some("publicChart.field.temperature"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -632,7 +620,10 @@ fn reading_points(rows: Vec<Reading>, field: &str) -> Vec<PublicChartPoint> {
|
||||
"target" => Some(row.target_temperature),
|
||||
_ => None,
|
||||
}?;
|
||||
value.is_finite().then_some(PublicChartPoint { timestamp: row.timestamp, value })
|
||||
value.is_finite().then_some(PublicChartPoint {
|
||||
timestamp: row.timestamp,
|
||||
value,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -649,7 +640,10 @@ fn zone_reading_points(rows: Vec<ZoneReading>, field: &str) -> Vec<PublicChartPo
|
||||
"outdoor" => row.outdoor_temperature,
|
||||
_ => None,
|
||||
}?;
|
||||
value.is_finite().then_some(PublicChartPoint { timestamp: row.timestamp, value })
|
||||
value.is_finite().then_some(PublicChartPoint {
|
||||
timestamp: row.timestamp,
|
||||
value,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -670,7 +664,11 @@ async fn public_custom_chart(
|
||||
validate_public_chart_spec(&share.series)?;
|
||||
|
||||
let hours = query.hours.unwrap_or(share.hours).clamp(1, 24 * 3650);
|
||||
let lang = if share.lang == "pl" { "pl" } else { "en" };
|
||||
let lang = LANGUAGE_ASSETS
|
||||
.iter()
|
||||
.find(|(code, _)| *code == share.lang.as_str())
|
||||
.map(|(code, _)| *code)
|
||||
.unwrap_or(DEFAULT_LANGUAGE_CODE);
|
||||
let since = Utc::now() - ChronoDuration::hours(hours);
|
||||
let bucket_seconds = history_bucket_seconds(hours);
|
||||
let limit = 20_000;
|
||||
@@ -683,43 +681,73 @@ async fn public_custom_chart(
|
||||
let kind = parts.next().unwrap_or_default();
|
||||
let id = parts.next().unwrap_or_default();
|
||||
let field = parts.next().unwrap_or_default();
|
||||
let field_label = public_chart_field_label(kind, field, lang)
|
||||
.ok_or_else(|| AppError::BadRequest(format!("unsupported custom chart series: {key}")))?;
|
||||
let label_key = public_chart_field_label_key(kind, field).ok_or_else(|| {
|
||||
AppError::BadRequest(format!("unsupported custom chart series: {key}"))
|
||||
})?;
|
||||
|
||||
let item = match kind {
|
||||
"device" => {
|
||||
let device = state.db.get_device(id)?
|
||||
let device = state
|
||||
.db
|
||||
.get_device(id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device {id}")))?;
|
||||
let (rows, _, _) = combined_device_history(&state, Some(id), since.clone(), bucket_seconds, limit).await?;
|
||||
let (rows, _, _) =
|
||||
combined_device_history(&state, Some(id), since.clone(), bucket_seconds, limit)
|
||||
.await?;
|
||||
PublicChartSeries {
|
||||
key: key.clone(),
|
||||
label: format!("{} · {}", device.name, field_label),
|
||||
label: device.name,
|
||||
label_key,
|
||||
dashed: field == "target",
|
||||
points: reading_points(rows, field),
|
||||
}
|
||||
}
|
||||
"installation" => {
|
||||
let group = state.db.get_device_group(id)?
|
||||
let group = state
|
||||
.db
|
||||
.get_device_group(id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device group {id}")))?;
|
||||
let representative = group.outdoor_temperature_device_id.as_deref()
|
||||
.filter(|device_id| group.device_ids.iter().any(|member| member.as_str() == *device_id))
|
||||
let representative = group
|
||||
.outdoor_temperature_device_id
|
||||
.as_deref()
|
||||
.filter(|device_id| {
|
||||
group
|
||||
.device_ids
|
||||
.iter()
|
||||
.any(|member| member.as_str() == *device_id)
|
||||
})
|
||||
.or_else(|| group.device_ids.first().map(String::as_str))
|
||||
.ok_or_else(|| AppError::BadRequest(format!("device group {id} has no devices")))?;
|
||||
let (rows, _, _) = combined_device_history(&state, Some(representative), since.clone(), bucket_seconds, limit).await?;
|
||||
.ok_or_else(|| {
|
||||
AppError::BadRequest(format!("device group {id} has no devices"))
|
||||
})?;
|
||||
let (rows, _, _) = combined_device_history(
|
||||
&state,
|
||||
Some(representative),
|
||||
since.clone(),
|
||||
bucket_seconds,
|
||||
limit,
|
||||
)
|
||||
.await?;
|
||||
PublicChartSeries {
|
||||
key: key.clone(),
|
||||
label: format!("{} · {}", group.name, field_label),
|
||||
label: group.name,
|
||||
label_key,
|
||||
dashed: false,
|
||||
points: reading_points(rows, "outdoor"),
|
||||
}
|
||||
}
|
||||
"zone" => {
|
||||
let zone = state.db.get_zone(id)?
|
||||
let zone = state
|
||||
.db
|
||||
.get_zone(id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("zone {id}")))?;
|
||||
let (rows, _, _) = combined_zone_history(&state, Some(id), since.clone(), bucket_seconds, limit).await?;
|
||||
let (rows, _, _) =
|
||||
combined_zone_history(&state, Some(id), since.clone(), bucket_seconds, limit)
|
||||
.await?;
|
||||
PublicChartSeries {
|
||||
key: key.clone(),
|
||||
label: format!("{} · {}", zone.name, field_label),
|
||||
label: zone.name,
|
||||
label_key,
|
||||
dashed: matches!(field, "target" | "device_target" | "outdoor"),
|
||||
points: zone_reading_points(rows, field),
|
||||
}
|
||||
@@ -732,36 +760,40 @@ async fn public_custom_chart(
|
||||
bucket_seconds,
|
||||
limit,
|
||||
&outdoor_entity,
|
||||
).await?;
|
||||
let alias = ha_settings.sensor_aliases.get(id)
|
||||
)
|
||||
.await?;
|
||||
let alias = ha_settings
|
||||
.sensor_aliases
|
||||
.get(id)
|
||||
.map(String::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or(id);
|
||||
PublicChartSeries {
|
||||
key: key.clone(),
|
||||
label: format!("HA · {} · {}", alias, field_label),
|
||||
label: format!("HA · {}", alias),
|
||||
label_key,
|
||||
dashed: true,
|
||||
points: rows.into_iter()
|
||||
points: rows
|
||||
.into_iter()
|
||||
.filter(|row| row.entity_id == id && row.temperature.is_finite())
|
||||
.map(|row| PublicChartPoint { timestamp: row.timestamp, value: row.temperature })
|
||||
.map(|row| PublicChartPoint {
|
||||
timestamp: row.timestamp,
|
||||
value: row.temperature,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
_ => return Err(AppError::BadRequest(format!("unsupported custom chart series: {key}"))),
|
||||
_ => {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"unsupported custom chart series: {key}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
series.push(item);
|
||||
}
|
||||
|
||||
let (hint, no_data_label) = if lang == "pl" {
|
||||
(format!("Ostatnie {hours} h"), "Brak danych")
|
||||
} else {
|
||||
(format!("Last {hours} h"), "No data")
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"title": share.title,
|
||||
"hint": hint,
|
||||
"no_data_label": no_data_label,
|
||||
"lang": lang,
|
||||
"hours": hours,
|
||||
"bucket_seconds": bucket_seconds,
|
||||
@@ -814,7 +846,12 @@ async fn energy_history(
|
||||
"previous_day" => Some(1),
|
||||
"previous_period" => Some(days),
|
||||
"previous_year" => Some(365),
|
||||
_ => return Err(AppError::BadRequest("energy compare must be none, previous_day, previous_period or previous_year".into())),
|
||||
_ => {
|
||||
return Err(AppError::BadRequest(
|
||||
"energy compare must be none, previous_day, previous_period or previous_year"
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let requested_source = query.source.as_deref().unwrap_or("auto");
|
||||
@@ -824,66 +861,75 @@ async fn energy_history(
|
||||
));
|
||||
}
|
||||
|
||||
let (target_type, public_target_id, target_name, member_device_ids, configured_source, storage_id, preferred_source) =
|
||||
if let Some(group_id) = target_id.strip_prefix("group:") {
|
||||
let group = state
|
||||
.db
|
||||
.get_device_group(group_id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device group {group_id}")))?;
|
||||
let auto_source = if group.energy_device_id.is_some() {
|
||||
Some("gree_cloud")
|
||||
} else if group.ha_energy_entity_id.is_some() {
|
||||
Some("home_assistant")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let selected = match requested_source {
|
||||
"gree_cloud" => Some("gree_cloud"),
|
||||
"home_assistant" => Some("home_assistant"),
|
||||
_ => match group.energy_source {
|
||||
EnergySourcePreference::GreeCloud => Some("gree_cloud"),
|
||||
EnergySourcePreference::HomeAssistant => Some("home_assistant"),
|
||||
EnergySourcePreference::Auto => auto_source,
|
||||
},
|
||||
};
|
||||
let storage_id = match selected {
|
||||
Some("gree_cloud") => group.energy_device_id.clone().ok_or_else(|| AppError::BadRequest("installation has no GREE Cloud energy source device".into()))?,
|
||||
_ => format!("group:{}", group.id),
|
||||
};
|
||||
(
|
||||
"group",
|
||||
format!("group:{}", group.id),
|
||||
group.name,
|
||||
group.device_ids,
|
||||
group.energy_source,
|
||||
storage_id,
|
||||
selected,
|
||||
)
|
||||
let (
|
||||
target_type,
|
||||
public_target_id,
|
||||
target_name,
|
||||
member_device_ids,
|
||||
configured_source,
|
||||
storage_id,
|
||||
preferred_source,
|
||||
) = if let Some(group_id) = target_id.strip_prefix("group:") {
|
||||
let group = state
|
||||
.db
|
||||
.get_device_group(group_id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device group {group_id}")))?;
|
||||
let auto_source = if group.energy_device_id.is_some() {
|
||||
Some("gree_cloud")
|
||||
} else if group.ha_energy_entity_id.is_some() {
|
||||
Some("home_assistant")
|
||||
} else {
|
||||
let device = state
|
||||
.db
|
||||
.get_device(target_id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device {target_id}")))?;
|
||||
let selected = match requested_source {
|
||||
"gree_cloud" => Some("gree_cloud"),
|
||||
"home_assistant" => Some("home_assistant"),
|
||||
"auto" => match device.energy_source {
|
||||
EnergySourcePreference::GreeCloud => Some("gree_cloud"),
|
||||
EnergySourcePreference::HomeAssistant => Some("home_assistant"),
|
||||
EnergySourcePreference::Auto => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
(
|
||||
"device",
|
||||
device.id.clone(),
|
||||
device.name,
|
||||
vec![device.id.clone()],
|
||||
device.energy_source,
|
||||
device.id,
|
||||
selected,
|
||||
)
|
||||
None
|
||||
};
|
||||
let selected = match requested_source {
|
||||
"gree_cloud" => Some("gree_cloud"),
|
||||
"home_assistant" => Some("home_assistant"),
|
||||
_ => match group.energy_source {
|
||||
EnergySourcePreference::GreeCloud => Some("gree_cloud"),
|
||||
EnergySourcePreference::HomeAssistant => Some("home_assistant"),
|
||||
EnergySourcePreference::Auto => auto_source,
|
||||
},
|
||||
};
|
||||
let storage_id = match selected {
|
||||
Some("gree_cloud") => group.energy_device_id.clone().ok_or_else(|| {
|
||||
AppError::BadRequest("installation has no GREE Cloud energy source device".into())
|
||||
})?,
|
||||
_ => format!("group:{}", group.id),
|
||||
};
|
||||
(
|
||||
"group",
|
||||
format!("group:{}", group.id),
|
||||
group.name,
|
||||
group.device_ids,
|
||||
group.energy_source,
|
||||
storage_id,
|
||||
selected,
|
||||
)
|
||||
} else {
|
||||
let device = state
|
||||
.db
|
||||
.get_device(target_id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device {target_id}")))?;
|
||||
let selected = match requested_source {
|
||||
"gree_cloud" => Some("gree_cloud"),
|
||||
"home_assistant" => Some("home_assistant"),
|
||||
"auto" => match device.energy_source {
|
||||
EnergySourcePreference::GreeCloud => Some("gree_cloud"),
|
||||
EnergySourcePreference::HomeAssistant => Some("home_assistant"),
|
||||
EnergySourcePreference::Auto => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
(
|
||||
"device",
|
||||
device.id.clone(),
|
||||
device.name,
|
||||
vec![device.id.clone()],
|
||||
device.energy_source,
|
||||
device.id,
|
||||
selected,
|
||||
)
|
||||
};
|
||||
|
||||
let now = Utc::now();
|
||||
let since = now - ChronoDuration::days(days);
|
||||
@@ -943,11 +989,15 @@ async fn energy_history(
|
||||
"InfluxDB energy history query failed",
|
||||
json!({"target_id": public_target_id, "storage_id": storage_id, "error": err.to_string()}),
|
||||
);
|
||||
state.db.list_energy_readings(&storage_id, load_since, limit)?
|
||||
state
|
||||
.db
|
||||
.list_energy_readings(&storage_id, load_since, limit)?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.db.list_energy_readings(&storage_id, load_since, limit)?
|
||||
state
|
||||
.db
|
||||
.list_energy_readings(&storage_id, load_since, limit)?
|
||||
};
|
||||
samples.sort_by_key(|row| row.timestamp);
|
||||
|
||||
@@ -976,18 +1026,27 @@ async fn energy_history(
|
||||
Utc,
|
||||
)
|
||||
}
|
||||
fn bucket_start(timestamp: chrono::DateTime<Utc>, interval_name: &str) -> chrono::DateTime<Utc> {
|
||||
fn bucket_start(
|
||||
timestamp: chrono::DateTime<Utc>,
|
||||
interval_name: &str,
|
||||
) -> chrono::DateTime<Utc> {
|
||||
let date = timestamp.date_naive();
|
||||
match interval_name {
|
||||
"hourly" => chrono::DateTime::<Utc>::from_naive_utc_and_offset(
|
||||
date.and_hms_opt(timestamp.hour(), 0, 0).expect("valid hour"),
|
||||
date.and_hms_opt(timestamp.hour(), 0, 0)
|
||||
.expect("valid hour"),
|
||||
Utc,
|
||||
),
|
||||
"weekly" => {
|
||||
let iso = date.iso_week();
|
||||
midnight(NaiveDate::from_isoywd_opt(iso.year(), iso.week(), Weekday::Mon).expect("valid ISO week"))
|
||||
midnight(
|
||||
NaiveDate::from_isoywd_opt(iso.year(), iso.week(), Weekday::Mon)
|
||||
.expect("valid ISO week"),
|
||||
)
|
||||
}
|
||||
"monthly" => midnight(NaiveDate::from_ymd_opt(date.year(), date.month(), 1).expect("valid month")),
|
||||
"monthly" => midnight(
|
||||
NaiveDate::from_ymd_opt(date.year(), date.month(), 1).expect("valid month"),
|
||||
),
|
||||
_ => midnight(date),
|
||||
}
|
||||
}
|
||||
@@ -999,18 +1058,26 @@ async fn energy_history(
|
||||
let mut buckets: BTreeMap<chrono::DateTime<Utc>, f64> = BTreeMap::new();
|
||||
for sample in rows {
|
||||
let shifted = sample.timestamp + ChronoDuration::days(shift_days);
|
||||
*buckets.entry(bucket_start(shifted, interval_name)).or_default() += sample.consumption_kwh.max(0.0);
|
||||
*buckets
|
||||
.entry(bucket_start(shifted, interval_name))
|
||||
.or_default() += sample.consumption_kwh.max(0.0);
|
||||
}
|
||||
buckets.into_iter().map(|(start, consumption_kwh)| json!({"start": start, "consumption_kwh": consumption_kwh.max(0.0)})).collect()
|
||||
}
|
||||
|
||||
let period_samples = samples.iter().filter(|row| row.timestamp >= since && row.timestamp <= now).collect::<Vec<_>>();
|
||||
let period_samples = samples
|
||||
.iter()
|
||||
.filter(|row| row.timestamp >= since && row.timestamp <= now)
|
||||
.collect::<Vec<_>>();
|
||||
let buckets = bucket_rows(period_samples.iter().copied(), interval_name, 0);
|
||||
|
||||
let comparison = if let Some(shift) = compare_shift_days {
|
||||
let compare_end = now - ChronoDuration::days(shift);
|
||||
let compare_start = since - ChronoDuration::days(shift);
|
||||
let rows = samples.iter().filter(|row| row.timestamp >= compare_start && row.timestamp <= compare_end).collect::<Vec<_>>();
|
||||
let rows = samples
|
||||
.iter()
|
||||
.filter(|row| row.timestamp >= compare_start && row.timestamp <= compare_end)
|
||||
.collect::<Vec<_>>();
|
||||
let total: f64 = rows.iter().map(|row| row.consumption_kwh.max(0.0)).sum();
|
||||
Some(json!({
|
||||
"kind": compare_name,
|
||||
@@ -1026,13 +1093,24 @@ async fn energy_history(
|
||||
|
||||
let today_start = midnight(now.date_naive());
|
||||
let yesterday_start = today_start - ChronoDuration::days(1);
|
||||
let month_start = midnight(NaiveDate::from_ymd_opt(now.year(), now.month(), 1).expect("valid current month"));
|
||||
let month_start =
|
||||
midnight(NaiveDate::from_ymd_opt(now.year(), now.month(), 1).expect("valid current month"));
|
||||
let previous_month_date = month_start.date_naive() - ChronoDuration::days(1);
|
||||
let previous_month_start = midnight(NaiveDate::from_ymd_opt(previous_month_date.year(), previous_month_date.month(), 1).expect("valid previous month"));
|
||||
let previous_month_start = midnight(
|
||||
NaiveDate::from_ymd_opt(previous_month_date.year(), previous_month_date.month(), 1)
|
||||
.expect("valid previous month"),
|
||||
);
|
||||
let sum_range = |start: chrono::DateTime<Utc>, stop: chrono::DateTime<Utc>| -> f64 {
|
||||
samples.iter().filter(|row| row.timestamp >= start && row.timestamp < stop).map(|row| row.consumption_kwh.max(0.0)).sum()
|
||||
samples
|
||||
.iter()
|
||||
.filter(|row| row.timestamp >= start && row.timestamp < stop)
|
||||
.map(|row| row.consumption_kwh.max(0.0))
|
||||
.sum()
|
||||
};
|
||||
let period_total: f64 = period_samples.iter().map(|row| row.consumption_kwh.max(0.0)).sum();
|
||||
let period_total: f64 = period_samples
|
||||
.iter()
|
||||
.map(|row| row.consumption_kwh.max(0.0))
|
||||
.sum();
|
||||
let latest = samples.last().cloned();
|
||||
|
||||
Ok(Json(json!({
|
||||
@@ -1079,7 +1157,9 @@ async fn combined_network_history(
|
||||
let cutoff = Utc::now() - ChronoDuration::days(influx.history_threshold_days.max(1) as i64);
|
||||
if !influx.enabled || since >= cutoff {
|
||||
return Ok((
|
||||
state.db.list_network_history(target_id, since, bucket_seconds, limit)?,
|
||||
state
|
||||
.db
|
||||
.list_network_history(target_id, since, bucket_seconds, limit)?,
|
||||
"sqlite".into(),
|
||||
None,
|
||||
));
|
||||
@@ -1105,17 +1185,27 @@ async fn combined_network_history(
|
||||
"InfluxDB connectivity history query failed",
|
||||
json!({"target_id": target_id, "error": err.to_string()}),
|
||||
);
|
||||
state.db.list_network_history(target_id, since, bucket_seconds, limit)?
|
||||
state
|
||||
.db
|
||||
.list_network_history(target_id, since, bucket_seconds, limit)?
|
||||
}
|
||||
};
|
||||
if warning.is_none() {
|
||||
values.extend(state.db.list_network_history(target_id, cutoff, bucket_seconds, limit)?);
|
||||
values.extend(
|
||||
state
|
||||
.db
|
||||
.list_network_history(target_id, cutoff, bucket_seconds, limit)?,
|
||||
);
|
||||
}
|
||||
values.sort_by_key(|row| row.timestamp);
|
||||
trim_history(&mut values, limit);
|
||||
Ok((
|
||||
values,
|
||||
if warning.is_some() { "sqlite_fallback".into() } else { "influx+sqlite".into() },
|
||||
if warning.is_some() {
|
||||
"sqlite_fallback".into()
|
||||
} else {
|
||||
"influx+sqlite".into()
|
||||
},
|
||||
warning,
|
||||
))
|
||||
}
|
||||
@@ -1149,10 +1239,12 @@ async fn network_history(
|
||||
}
|
||||
let has_rest = readings.iter().any(|row| row.target_id == "cloud:rest");
|
||||
let has_mqtt = readings.iter().any(|row| row.target_id == "cloud:mqtt");
|
||||
if (settings.gree_cloud.enabled && settings.gree_cloud.connectivity_metrics_enabled) || has_rest {
|
||||
if (settings.gree_cloud.enabled && settings.gree_cloud.connectivity_metrics_enabled) || has_rest
|
||||
{
|
||||
targets.push(json!({"id":"cloud:rest","name":"GREE Cloud REST","kind":"cloud_service","source":"cloud_rest"}));
|
||||
}
|
||||
if (settings.gree_cloud.enabled && settings.gree_cloud.connectivity_metrics_enabled) || has_mqtt {
|
||||
if (settings.gree_cloud.enabled && settings.gree_cloud.connectivity_metrics_enabled) || has_mqtt
|
||||
{
|
||||
targets.push(json!({"id":"cloud:mqtt","name":"GREE Cloud MQTT","kind":"cloud_service","source":"cloud_mqtt"}));
|
||||
}
|
||||
Ok(Json(json!({
|
||||
|
||||
Reference in New Issue
Block a user