#[derive(Debug, serde::Serialize)] struct HouseSnapshot { mode: String, emergency_stop_enabled: bool, emergency_stop_since: Option>, } #[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, } #[derive(Debug, Clone, serde::Serialize)] struct DeviceGroupEnergySnapshot { total_kwh: f64, timestamp: Option>, source: String, origin: String, } fn device_group_energy_snapshot( state: &AppState, group: &DeviceGroup, devices: &[Device], ) -> Result, 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, zones: Vec, groups: Vec, device_groups: Vec, device_group_energy: std::collections::HashMap, schedules: Vec, automations: Vec, flows: Vec, access_tokens: Vec, settings: SettingsSnapshot, house: HouseSnapshot, outdoor_temperature: Option, control_plan: crate::models::ControlPlan, control_plan_revision: u64, system: SystemInfoResponse, } async fn health(State(state): State, request: Request) -> Result, AppError> { if home_assistant::supervisor_token_detected() { let trusted_supervisor = request .extensions() .get::>() .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) -> Result, AppError> { Ok(Json(build_bootstrap(&state).await?)) } async fn build_bootstrap(state: &AppState) -> Result { 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) -> Result, AppError> { let devices = state.db.list_devices()?; Ok(Json(build_system_info(&state, &devices))) }