222 lines
8.1 KiB
Rust
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)))
|
|
}
|