v0.12.0-preety_code
This commit is contained in:
+262
-68
@@ -1,8 +1,19 @@
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ReadingsQuery { device_id: Option<String>, hours: Option<i64>, limit: Option<u32> }
|
||||
async fn readings(State(state): State<AppState>, Query(query): Query<ReadingsQuery>) -> Result<Json<Value>, AppError> {
|
||||
struct ReadingsQuery {
|
||||
device_id: Option<String>,
|
||||
hours: Option<i64>,
|
||||
limit: Option<u32>,
|
||||
}
|
||||
async fn readings(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<ReadingsQuery>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let hours = query.hours.unwrap_or(24).clamp(1, 24 * 3650);
|
||||
let values = state.db.list_readings(query.device_id.as_deref(), Utc::now() - ChronoDuration::hours(hours), query.limit.unwrap_or(1500))?;
|
||||
let values = state.db.list_readings(
|
||||
query.device_id.as_deref(),
|
||||
Utc::now() - ChronoDuration::hours(hours),
|
||||
query.limit.unwrap_or(1500),
|
||||
)?;
|
||||
Ok(Json(json!({"readings": values})))
|
||||
}
|
||||
|
||||
@@ -29,24 +40,27 @@ fn history_bucket_seconds(hours: i64) -> i64 {
|
||||
}
|
||||
|
||||
fn fallback_zone_rows(zone: &Zone, device: &Device, readings: Vec<Reading>) -> Vec<ZoneReading> {
|
||||
readings.into_iter().map(|reading| ZoneReading {
|
||||
id: reading.id,
|
||||
zone_id: zone.id.clone(),
|
||||
device_id: zone.device_id.clone(),
|
||||
timestamp: reading.timestamp,
|
||||
gree_temperature: reading.indoor_temperature,
|
||||
external_temperature: None,
|
||||
control_temperature: reading.indoor_temperature,
|
||||
target_temperature: Some(reading.target_temperature),
|
||||
device_setpoint: Some(reading.target_temperature),
|
||||
outdoor_temperature: reading.outdoor_temperature,
|
||||
power: reading.power,
|
||||
mode: device.mode.clone(),
|
||||
fan_speed: device.fan_speed,
|
||||
demand: false,
|
||||
control_source: "gree_history_fallback".into(),
|
||||
active_preset: "history".into(),
|
||||
}).collect()
|
||||
readings
|
||||
.into_iter()
|
||||
.map(|reading| ZoneReading {
|
||||
id: reading.id,
|
||||
zone_id: zone.id.clone(),
|
||||
device_id: zone.device_id.clone(),
|
||||
timestamp: reading.timestamp,
|
||||
gree_temperature: reading.indoor_temperature,
|
||||
external_temperature: None,
|
||||
control_temperature: reading.indoor_temperature,
|
||||
target_temperature: Some(reading.target_temperature),
|
||||
device_setpoint: Some(reading.target_temperature),
|
||||
outdoor_temperature: reading.outdoor_temperature,
|
||||
power: reading.power,
|
||||
mode: device.mode.clone(),
|
||||
fan_speed: device.fan_speed,
|
||||
demand: false,
|
||||
control_source: "gree_history_fallback".into(),
|
||||
active_preset: "history".into(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn zone_history_with_fallback(
|
||||
@@ -56,23 +70,43 @@ fn zone_history_with_fallback(
|
||||
bucket_seconds: i64,
|
||||
limit: u32,
|
||||
) -> Result<Vec<ZoneReading>, AppError> {
|
||||
let mut values = state.db.list_zone_history(zone_id, since.clone(), bucket_seconds, limit)?;
|
||||
let mut values = state
|
||||
.db
|
||||
.list_zone_history(zone_id, since.clone(), bucket_seconds, limit)?;
|
||||
if let Some(zone_id) = zone_id {
|
||||
if values.is_empty() {
|
||||
let zone = state.db.get_zone(zone_id)?.ok_or_else(|| AppError::NotFound(format!("zone {zone_id}")))?;
|
||||
let zone = state
|
||||
.db
|
||||
.get_zone(zone_id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("zone {zone_id}")))?;
|
||||
if let Some(device) = state.db.get_device(&zone.device_id)? {
|
||||
let rows = state.db.list_device_history(Some(&zone.device_id), since.clone(), bucket_seconds, limit)?;
|
||||
let rows = state.db.list_device_history(
|
||||
Some(&zone.device_id),
|
||||
since.clone(),
|
||||
bucket_seconds,
|
||||
limit,
|
||||
)?;
|
||||
values = fallback_zone_rows(&zone, &device, rows);
|
||||
}
|
||||
}
|
||||
return Ok(values);
|
||||
}
|
||||
|
||||
let existing: std::collections::HashSet<String> = values.iter().map(|row| row.zone_id.clone()).collect();
|
||||
let existing: std::collections::HashSet<String> =
|
||||
values.iter().map(|row| row.zone_id.clone()).collect();
|
||||
for zone in state.db.list_zones()? {
|
||||
if existing.contains(&zone.id) { continue; }
|
||||
let Some(device) = state.db.get_device(&zone.device_id)? else { continue; };
|
||||
let rows = state.db.list_device_history(Some(&zone.device_id), since.clone(), bucket_seconds, limit)?;
|
||||
if existing.contains(&zone.id) {
|
||||
continue;
|
||||
}
|
||||
let Some(device) = state.db.get_device(&zone.device_id)? else {
|
||||
continue;
|
||||
};
|
||||
let rows = state.db.list_device_history(
|
||||
Some(&zone.device_id),
|
||||
since.clone(),
|
||||
bucket_seconds,
|
||||
limit,
|
||||
)?;
|
||||
values.extend(fallback_zone_rows(&zone, &device, rows));
|
||||
}
|
||||
values.sort_by(|left, right| left.timestamp.cmp(&right.timestamp));
|
||||
@@ -90,33 +124,68 @@ fn sensor_history_with_fallback(
|
||||
limit: u32,
|
||||
outdoor_entity: &str,
|
||||
) -> Result<Vec<HaReading>, AppError> {
|
||||
let mut values = state.db.list_ha_history(None, since.clone(), bucket_seconds, limit)?;
|
||||
let mut existing: std::collections::HashSet<String> = values.iter().map(|row| row.entity_id.clone()).collect();
|
||||
let mut values = state
|
||||
.db
|
||||
.list_ha_history(None, since.clone(), bucket_seconds, limit)?;
|
||||
let mut existing: std::collections::HashSet<String> =
|
||||
values.iter().map(|row| row.entity_id.clone()).collect();
|
||||
for zone in state.db.list_zones()? {
|
||||
let Some(entity_id) = zone.ha_entity_id.as_deref().filter(|value| !value.trim().is_empty()) else { continue; };
|
||||
if existing.contains(entity_id) { continue; }
|
||||
let rows = state.db.list_zone_history(Some(&zone.id), since.clone(), bucket_seconds, limit)?;
|
||||
let Some(entity_id) = zone
|
||||
.ha_entity_id
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if existing.contains(entity_id) {
|
||||
continue;
|
||||
}
|
||||
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.external_temperature {
|
||||
values.push(HaReading { id: row.id, entity_id: entity_id.to_string(), zone_id: Some(zone.id.clone()), kind: "room".into(), timestamp: row.timestamp, temperature });
|
||||
values.push(HaReading {
|
||||
id: row.id,
|
||||
entity_id: entity_id.to_string(),
|
||||
zone_id: Some(zone.id.clone()),
|
||||
kind: "room".into(),
|
||||
timestamp: row.timestamp,
|
||||
temperature,
|
||||
});
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
if added { existing.insert(entity_id.to_string()); }
|
||||
if added {
|
||||
existing.insert(entity_id.to_string());
|
||||
}
|
||||
}
|
||||
let outdoor_entity = outdoor_entity.trim();
|
||||
if !outdoor_entity.is_empty() && !existing.contains(outdoor_entity) {
|
||||
for zone in state.db.list_zones()? {
|
||||
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 {
|
||||
values.push(HaReading { id: row.id, entity_id: outdoor_entity.to_string(), zone_id: None, kind: "outdoor".into(), timestamp: row.timestamp, temperature });
|
||||
values.push(HaReading {
|
||||
id: row.id,
|
||||
entity_id: outdoor_entity.to_string(),
|
||||
zone_id: None,
|
||||
kind: "outdoor".into(),
|
||||
timestamp: row.timestamp,
|
||||
temperature,
|
||||
});
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
if added { break; }
|
||||
if added {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
values.sort_by(|left, right| left.timestamp.cmp(&right.timestamp));
|
||||
@@ -137,23 +206,54 @@ async fn combined_device_history(
|
||||
let influx = state.settings.read().await.influxdb.clone();
|
||||
let cutoff = Utc::now() - ChronoDuration::days(influx.history_threshold_days.max(1) as i64);
|
||||
if !influx.enabled || since >= cutoff {
|
||||
return Ok((state.db.list_device_history(device_id, since, bucket_seconds, limit)?, "sqlite".into(), None));
|
||||
return Ok((
|
||||
state
|
||||
.db
|
||||
.list_device_history(device_id, since, bucket_seconds, limit)?,
|
||||
"sqlite".into(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
let mut warning = None;
|
||||
let mut values = match influxdb::query_devices(&state.http, &influx, device_id, since, cutoff, bucket_seconds, limit).await {
|
||||
let mut values = match influxdb::query_devices(
|
||||
&state.http,
|
||||
&influx,
|
||||
device_id,
|
||||
since,
|
||||
cutoff,
|
||||
bucket_seconds,
|
||||
limit,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(err) => {
|
||||
warning = Some(err.to_string());
|
||||
state.log("warn", "influx.query_error", "InfluxDB device history query failed", json!({"error": err.to_string()}));
|
||||
state.db.list_device_history(device_id, since, bucket_seconds, limit)?
|
||||
state.log(
|
||||
"warn",
|
||||
"influx.query_error",
|
||||
"InfluxDB device history query failed",
|
||||
json!({"error": err.to_string()}),
|
||||
);
|
||||
state
|
||||
.db
|
||||
.list_device_history(device_id, since, bucket_seconds, limit)?
|
||||
}
|
||||
};
|
||||
if warning.is_none() {
|
||||
values.extend(state.db.list_device_history(device_id, cutoff, bucket_seconds, limit)?);
|
||||
values.extend(
|
||||
state
|
||||
.db
|
||||
.list_device_history(device_id, cutoff, bucket_seconds, limit)?,
|
||||
);
|
||||
}
|
||||
values.sort_by_key(|row| row.timestamp);
|
||||
trim_history(&mut values, limit);
|
||||
let source = if warning.is_some() { "sqlite_fallback" } else { "influx+sqlite" };
|
||||
let source = if warning.is_some() {
|
||||
"sqlite_fallback"
|
||||
} else {
|
||||
"influx+sqlite"
|
||||
};
|
||||
Ok((values, source.into(), warning))
|
||||
}
|
||||
|
||||
@@ -167,23 +267,52 @@ async fn combined_zone_history(
|
||||
let influx = state.settings.read().await.influxdb.clone();
|
||||
let cutoff = Utc::now() - ChronoDuration::days(influx.history_threshold_days.max(1) as i64);
|
||||
if !influx.enabled || since >= cutoff {
|
||||
return Ok((zone_history_with_fallback(state, zone_id, since, bucket_seconds, limit)?, "sqlite".into(), None));
|
||||
return Ok((
|
||||
zone_history_with_fallback(state, zone_id, since, bucket_seconds, limit)?,
|
||||
"sqlite".into(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
let mut warning = None;
|
||||
let mut values = match influxdb::query_zones(&state.http, &influx, zone_id, since, cutoff, bucket_seconds, limit).await {
|
||||
let mut values = match influxdb::query_zones(
|
||||
&state.http,
|
||||
&influx,
|
||||
zone_id,
|
||||
since,
|
||||
cutoff,
|
||||
bucket_seconds,
|
||||
limit,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(err) => {
|
||||
warning = Some(err.to_string());
|
||||
state.log("warn", "influx.query_error", "InfluxDB zone history query failed", json!({"error": err.to_string()}));
|
||||
state.log(
|
||||
"warn",
|
||||
"influx.query_error",
|
||||
"InfluxDB zone history query failed",
|
||||
json!({"error": err.to_string()}),
|
||||
);
|
||||
zone_history_with_fallback(state, zone_id, since, bucket_seconds, limit)?
|
||||
}
|
||||
};
|
||||
if warning.is_none() {
|
||||
values.extend(zone_history_with_fallback(state, zone_id, cutoff, bucket_seconds, limit)?);
|
||||
values.extend(zone_history_with_fallback(
|
||||
state,
|
||||
zone_id,
|
||||
cutoff,
|
||||
bucket_seconds,
|
||||
limit,
|
||||
)?);
|
||||
}
|
||||
values.sort_by_key(|row| row.timestamp);
|
||||
trim_history(&mut values, limit);
|
||||
let source = if warning.is_some() { "sqlite_fallback" } else { "influx+sqlite" };
|
||||
let source = if warning.is_some() {
|
||||
"sqlite_fallback"
|
||||
} else {
|
||||
"influx+sqlite"
|
||||
};
|
||||
Ok((values, source.into(), warning))
|
||||
}
|
||||
|
||||
@@ -198,18 +327,38 @@ async fn combined_sensor_history(
|
||||
let influx = state.settings.read().await.influxdb.clone();
|
||||
let cutoff = Utc::now() - ChronoDuration::days(influx.history_threshold_days.max(1) as i64);
|
||||
let local = |start| -> Result<Vec<HaReading>, AppError> {
|
||||
if entity_id.is_some() { Ok(state.db.list_ha_history(entity_id, start, bucket_seconds, limit)?) }
|
||||
else { sensor_history_with_fallback(state, start, bucket_seconds, limit, outdoor_entity) }
|
||||
if entity_id.is_some() {
|
||||
Ok(state
|
||||
.db
|
||||
.list_ha_history(entity_id, start, bucket_seconds, limit)?)
|
||||
} else {
|
||||
sensor_history_with_fallback(state, start, bucket_seconds, limit, outdoor_entity)
|
||||
}
|
||||
};
|
||||
if !influx.enabled || since >= cutoff {
|
||||
return Ok((local(since)?, "sqlite".into(), None));
|
||||
}
|
||||
let mut warning = None;
|
||||
let mut values = match influxdb::query_ha(&state.http, &influx, entity_id, since, cutoff, bucket_seconds, limit).await {
|
||||
let mut values = match influxdb::query_ha(
|
||||
&state.http,
|
||||
&influx,
|
||||
entity_id,
|
||||
since,
|
||||
cutoff,
|
||||
bucket_seconds,
|
||||
limit,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(err) => {
|
||||
warning = Some(err.to_string());
|
||||
state.log("warn", "influx.query_error", "InfluxDB HA history query failed", json!({"error": err.to_string()}));
|
||||
state.log(
|
||||
"warn",
|
||||
"influx.query_error",
|
||||
"InfluxDB HA history query failed",
|
||||
json!({"error": err.to_string()}),
|
||||
);
|
||||
local(since)?
|
||||
}
|
||||
};
|
||||
@@ -218,7 +367,11 @@ async fn combined_sensor_history(
|
||||
}
|
||||
values.sort_by_key(|row| row.timestamp);
|
||||
trim_history(&mut values, limit);
|
||||
let source = if warning.is_some() { "sqlite_fallback" } else { "influx+sqlite" };
|
||||
let source = if warning.is_some() {
|
||||
"sqlite_fallback"
|
||||
} else {
|
||||
"influx+sqlite"
|
||||
};
|
||||
Ok((values, source.into(), warning))
|
||||
}
|
||||
|
||||
@@ -229,19 +382,32 @@ fn trim_history<T>(values: &mut Vec<T>, limit: u32) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn history(State(state): State<AppState>, Query(query): Query<HistoryQuery>) -> Result<Json<Value>, AppError> {
|
||||
async fn history(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<HistoryQuery>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let hours = query.hours.unwrap_or(24).clamp(1, 24 * 3650);
|
||||
let since = Utc::now() - ChronoDuration::hours(hours);
|
||||
let bucket_seconds = history_bucket_seconds(hours);
|
||||
let limit = query.limit.unwrap_or(12_000).clamp(1, 20_000);
|
||||
let scope = query.scope.as_deref().unwrap_or("zones");
|
||||
let outdoor_entity = state.settings.read().await.home_assistant.outdoor_entity_id.clone();
|
||||
let outdoor_entity = state
|
||||
.settings
|
||||
.read()
|
||||
.await
|
||||
.home_assistant
|
||||
.outdoor_entity_id
|
||||
.clone();
|
||||
let (device_count, zone_count, ha_count) = state.db.history_counts()?;
|
||||
|
||||
match scope {
|
||||
"devices" => {
|
||||
let device_id = query.device_id.as_deref().filter(|value| !value.is_empty() && *value != "all");
|
||||
let (readings, storage, warning) = combined_device_history(&state, device_id, since, bucket_seconds, limit).await?;
|
||||
let device_id = query
|
||||
.device_id
|
||||
.as_deref()
|
||||
.filter(|value| !value.is_empty() && *value != "all");
|
||||
let (readings, storage, warning) =
|
||||
combined_device_history(&state, device_id, since, bucket_seconds, limit).await?;
|
||||
Ok(Json(json!({
|
||||
"scope": "devices", "readings": readings, "bucket_seconds": bucket_seconds,
|
||||
"storage": storage, "storage_warning": warning,
|
||||
@@ -249,8 +415,19 @@ async fn history(State(state): State<AppState>, Query(query): Query<HistoryQuery
|
||||
})))
|
||||
}
|
||||
"sensors" => {
|
||||
let entity_id = query.entity_id.as_deref().filter(|value| !value.is_empty() && *value != "all");
|
||||
let (readings, storage, warning) = combined_sensor_history(&state, entity_id, since, bucket_seconds, limit, &outdoor_entity).await?;
|
||||
let entity_id = query
|
||||
.entity_id
|
||||
.as_deref()
|
||||
.filter(|value| !value.is_empty() && *value != "all");
|
||||
let (readings, storage, warning) = combined_sensor_history(
|
||||
&state,
|
||||
entity_id,
|
||||
since,
|
||||
bucket_seconds,
|
||||
limit,
|
||||
&outdoor_entity,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!({
|
||||
"scope": "sensors", "readings": readings, "bucket_seconds": bucket_seconds,
|
||||
"storage": storage, "storage_warning": warning,
|
||||
@@ -258,9 +435,19 @@ async fn history(State(state): State<AppState>, Query(query): Query<HistoryQuery
|
||||
})))
|
||||
}
|
||||
"overview" => {
|
||||
let (zones, zone_storage, zone_warning) = combined_zone_history(&state, None, since, bucket_seconds, limit).await?;
|
||||
let (devices, device_storage, device_warning) = combined_device_history(&state, None, since, bucket_seconds, limit).await?;
|
||||
let (sensors, sensor_storage, sensor_warning) = combined_sensor_history(&state, None, since, bucket_seconds, limit, &outdoor_entity).await?;
|
||||
let (zones, zone_storage, zone_warning) =
|
||||
combined_zone_history(&state, None, since, bucket_seconds, limit).await?;
|
||||
let (devices, device_storage, device_warning) =
|
||||
combined_device_history(&state, None, since, bucket_seconds, limit).await?;
|
||||
let (sensors, sensor_storage, sensor_warning) = combined_sensor_history(
|
||||
&state,
|
||||
None,
|
||||
since,
|
||||
bucket_seconds,
|
||||
limit,
|
||||
&outdoor_entity,
|
||||
)
|
||||
.await?;
|
||||
let storage_warning = [zone_warning, device_warning, sensor_warning]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -274,24 +461,31 @@ async fn history(State(state): State<AppState>, Query(query): Query<HistoryQuery
|
||||
})))
|
||||
}
|
||||
"zones" | "zone" => {
|
||||
let zone_id = query.zone_id.as_deref().filter(|value| !value.is_empty() && *value != "all");
|
||||
let zone_id = query
|
||||
.zone_id
|
||||
.as_deref()
|
||||
.filter(|value| !value.is_empty() && *value != "all");
|
||||
if let Some(zone_id) = zone_id {
|
||||
if state.db.get_zone(zone_id)?.is_none() {
|
||||
return Err(AppError::NotFound(format!("zone {zone_id}")));
|
||||
}
|
||||
}
|
||||
let (readings, storage, warning) = combined_zone_history(&state, zone_id, since, bucket_seconds, limit).await?;
|
||||
let (readings, storage, warning) =
|
||||
combined_zone_history(&state, zone_id, since, bucket_seconds, limit).await?;
|
||||
Ok(Json(json!({
|
||||
"scope": "zones", "readings": readings, "bucket_seconds": bucket_seconds,
|
||||
"storage": storage, "storage_warning": warning,
|
||||
"counts": {"devices": device_count, "zones": zone_count, "ha": ha_count}
|
||||
})))
|
||||
}
|
||||
_ => Err(AppError::BadRequest("history scope must be overview, zones, devices or sensors".into())),
|
||||
_ => Err(AppError::BadRequest(
|
||||
"history scope must be overview, zones, devices or sensors".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn control_plan(State(state): State<AppState>) -> Result<Json<Value>, AppError> {
|
||||
Ok(Json(serde_json::to_value(engine::build_control_plan(&state).await?)?))
|
||||
Ok(Json(serde_json::to_value(
|
||||
engine::build_control_plan(&state).await?,
|
||||
)?))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user