Files
gree-controller/src/api/system.rs
T
2026-09-21 22:58:30 +02:00

222 lines
8.1 KiB
Rust

#[derive(Debug, serde::Serialize)]
struct HouseSnapshot {
mode: String,
emergency_stop_enabled: bool,
emergency_stop_since: Option<chrono::DateTime<Utc>>,
}
#[derive(Debug, serde::Serialize)]
struct SystemInfoResponse {
version: &'static str,
uptime_seconds: u64,
auth_required: bool,
control_ready: bool,
database: String,
device_count: usize,
online_count: usize,
simulator_count: usize,
bind: String,
base_path: String,
public_chart_base_url: String,
gree_interface: String,
gree_received_frames: u64,
gree_received_frames_by_device: std::collections::HashMap<String, u64>,
}
#[derive(Debug, Clone, serde::Serialize)]
struct DeviceGroupEnergySnapshot {
total_kwh: f64,
timestamp: Option<chrono::DateTime<Utc>>,
source: String,
origin: String,
}
fn device_group_energy_snapshot(
state: &AppState,
group: &DeviceGroup,
devices: &[Device],
) -> Result<Option<DeviceGroupEnergySnapshot>, AppError> {
let selected_source = match group.energy_source {
EnergySourcePreference::GreeCloud => Some("gree_cloud"),
EnergySourcePreference::HomeAssistant => Some("home_assistant"),
EnergySourcePreference::Auto if group.energy_device_id.is_some() => Some("gree_cloud"),
EnergySourcePreference::Auto if group.ha_energy_entity_id.is_some() => {
Some("home_assistant")
}
EnergySourcePreference::Auto => None,
};
match selected_source {
Some("gree_cloud") => {
let Some(device_id) = group.energy_device_id.as_deref() else {
return Ok(None);
};
if let Some(device) = devices.iter().find(|device| device.id == device_id) {
if let Some(total_kwh) = device
.total_energy_kwh
.filter(|value| value.is_finite() && *value >= 0.0)
{
return Ok(Some(DeviceGroupEnergySnapshot {
total_kwh,
timestamp: device
.last_cloud_sync
.clone()
.or_else(|| device.last_seen.clone()),
source: "gree_cloud".into(),
origin: "cloud".into(),
}));
}
}
Ok(state
.db
.last_energy_reading(device_id, "gree_cloud")?
.map(|reading| DeviceGroupEnergySnapshot {
total_kwh: reading.normalized_meter_kwh,
timestamp: Some(reading.timestamp),
source: "gree_cloud".into(),
origin: "database".into(),
}))
}
Some("home_assistant") => {
let storage_id = format!("group:{}", group.id);
Ok(state
.db
.last_energy_reading(&storage_id, "home_assistant")?
.map(|reading| DeviceGroupEnergySnapshot {
total_kwh: reading.normalized_meter_kwh,
timestamp: Some(reading.timestamp),
source: "home_assistant".into(),
origin: "database".into(),
}))
}
_ => Ok(None),
}
}
#[derive(Debug, serde::Serialize)]
struct BootstrapResponse {
devices: Vec<Device>,
zones: Vec<Zone>,
groups: Vec<ClimateGroup>,
device_groups: Vec<DeviceGroup>,
device_group_energy: std::collections::HashMap<String, DeviceGroupEnergySnapshot>,
schedules: Vec<Schedule>,
automations: Vec<Automation>,
flows: Vec<Flow>,
access_tokens: Vec<ApiTokenInfo>,
settings: SettingsSnapshot,
house: HouseSnapshot,
outdoor_temperature: Option<f64>,
control_plan: crate::models::ControlPlan,
control_plan_revision: u64,
system: SystemInfoResponse,
}
async fn health(State(state): State<AppState>, request: Request) -> Result<Json<Value>, AppError> {
if home_assistant::supervisor_token_detected() {
let trusted_supervisor = request
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|info| home_assistant::is_supervisor_ingress_peer(info.0.ip()))
.unwrap_or(false);
if !trusted_supervisor {
let expected = state.config.app_token.trim();
if expected.is_empty() || request_token(&request).as_deref() != Some(expected) {
return Err(AppError::Unauthorized);
}
}
}
Ok(Json(json!({
"status": "ok",
"name": "gree-controller",
"version": env!("CARGO_PKG_VERSION"),
"uptime_seconds": state.started.elapsed().as_secs(),
"control_ready": state.initial_device_sync_complete.load(Ordering::Acquire),
"time": Utc::now(),
})))
}
async fn bootstrap(State(state): State<AppState>) -> Result<Json<BootstrapResponse>, AppError> {
Ok(Json(build_bootstrap(&state).await?))
}
async fn build_bootstrap(state: &AppState) -> Result<BootstrapResponse, AppError> {
let (settings, house_mode, emergency_stop_enabled, emergency_stop_since) = {
let settings = state.settings.read().await;
(
settings_snapshot(&settings),
settings.house_mode.clone(),
settings.emergency_stop_enabled,
settings.emergency_stop_since.clone(),
)
};
let devices = state.db.list_devices()?;
let device_groups = state.db.list_device_groups()?;
let mut device_group_energy = std::collections::HashMap::new();
for group in &device_groups {
if let Some(snapshot) = device_group_energy_snapshot(state, group, &devices)? {
device_group_energy.insert(group.id.clone(), snapshot);
}
}
let system = build_system_info(state, &devices);
let control_plan = engine::get_control_plan_snapshot(state).await?;
Ok(BootstrapResponse {
devices,
zones: state.db.list_zones()?,
groups: state.db.list_groups()?,
device_groups,
device_group_energy,
schedules: state.db.list_schedules()?,
automations: state.db.list_automations()?,
flows: state.db.list_flows()?,
access_tokens: state.db.list_api_tokens()?,
settings,
house: HouseSnapshot {
mode: house_mode,
emergency_stop_enabled,
emergency_stop_since,
},
outdoor_temperature: *state.outdoor_temperature.read().await,
control_plan: control_plan.plan.as_ref().clone(),
control_plan_revision: control_plan.revision,
system,
})
}
fn build_system_info(state: &AppState, devices: &[Device]) -> SystemInfoResponse {
let (received_frames_total, received_frames_by_device) =
state.providers.local().client().received_frame_stats();
SystemInfoResponse {
version: env!("CARGO_PKG_VERSION"),
uptime_seconds: state.started.elapsed().as_secs(),
auth_required: !state.config.app_token.trim().is_empty()
|| home_assistant::supervisor_token_detected(),
control_ready: state.initial_device_sync_complete.load(Ordering::Acquire),
database: state.config.database.display().to_string(),
device_count: devices.len(),
online_count: devices.iter().filter(|device| device.online).count(),
simulator_count: devices.iter().filter(|device| device.simulated).count(),
bind: state.config.bind.to_string(),
base_path: if state.config.base_path.is_empty() {
"/".to_string()
} else {
state.config.base_path.clone()
},
public_chart_base_url: state.config.public_chart_base_url.clone(),
gree_interface: if state.config.gree_interface.trim().is_empty() {
"auto".to_string()
} else {
state.config.gree_interface.trim().to_string()
},
gree_received_frames: received_frames_total,
gree_received_frames_by_device: received_frames_by_device,
}
}
async fn system_info(State(state): State<AppState>) -> Result<Json<SystemInfoResponse>, AppError> {
let devices = state.db.list_devices()?;
Ok(Json(build_system_info(&state, &devices)))
}