207 lines
7.6 KiB
Rust
207 lines
7.6 KiB
Rust
fn map_home_assistant_integration_error(error: anyhow::Error) -> AppError {
|
|
let message = error.to_string();
|
|
if message.contains("Home Assistant URL is not configured")
|
|
|| message.contains("Home Assistant token is not configured")
|
|
|| message.contains("Home Assistant Supervisor token is not available")
|
|
|| message.contains("invalid Home Assistant URL")
|
|
|| message.contains("Home Assistant URL must use http or https")
|
|
|| message.contains("cannot build Home Assistant API URL")
|
|
|| message.contains("cannot build Home Assistant service URL")
|
|
{
|
|
AppError::BadRequest(message)
|
|
} else {
|
|
AppError::Dependency(message)
|
|
}
|
|
}
|
|
|
|
fn map_notification_test_error(message: String) -> AppError {
|
|
if matches!(
|
|
message.as_str(),
|
|
"Pushover credentials are incomplete"
|
|
| "invalid webhook URL"
|
|
| "webhook URL must use HTTPS"
|
|
| "webhook host is not supported"
|
|
| "unsupported notification provider"
|
|
) {
|
|
AppError::BadRequest(message)
|
|
} else {
|
|
AppError::Dependency(message)
|
|
}
|
|
}
|
|
|
|
async fn home_assistant_snapshot(
|
|
State(state): State<AppState>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
let control_plan = engine::get_control_plan_snapshot(&state).await?;
|
|
let groups = list_home_assistant_groups(State(state.clone())).await?.0;
|
|
Ok(Json(json!({
|
|
"devices": state.db.list_devices()?,
|
|
"control_plan": control_plan.plan.as_ref(),
|
|
"control_plan_revision": control_plan.revision,
|
|
"groups": groups,
|
|
})))
|
|
}
|
|
|
|
async fn test_home_assistant(
|
|
State(state): State<AppState>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
let settings = state.settings.read().await.clone();
|
|
let sample = home_assistant::test_connection(&state.http, &settings.home_assistant)
|
|
.await
|
|
.map_err(map_home_assistant_integration_error)?;
|
|
Ok(Json(json!({
|
|
"ok": true,
|
|
"auth_mode": home_assistant::auth_mode(&settings.home_assistant),
|
|
"sample": sample,
|
|
})))
|
|
}
|
|
|
|
async fn list_home_assistant_entities(
|
|
State(state): State<AppState>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
let settings = state.settings.read().await.home_assistant.clone();
|
|
if !home_assistant::token_configured(&settings)
|
|
|| home_assistant::effective_url(&settings).trim().is_empty()
|
|
{
|
|
return Ok(Json(json!({
|
|
"configured": false,
|
|
"entities": [],
|
|
})));
|
|
}
|
|
|
|
let mut entities = home_assistant::list_entities(&state.http, &settings)
|
|
.await
|
|
.map_err(map_home_assistant_integration_error)?
|
|
.into_iter()
|
|
.filter_map(|entity| {
|
|
let entity_id = entity.get("entity_id")?.as_str()?.trim();
|
|
if entity_id.is_empty() {
|
|
return None;
|
|
}
|
|
let attributes = entity.get("attributes").and_then(Value::as_object);
|
|
Some(json!({
|
|
"entity_id": entity_id,
|
|
"name": attributes.and_then(|value| value.get("friendly_name")).and_then(Value::as_str).unwrap_or_default(),
|
|
"state": entity.get("state").and_then(Value::as_str).unwrap_or_default(),
|
|
"unit": attributes.and_then(|value| value.get("unit_of_measurement")).and_then(Value::as_str).unwrap_or_default(),
|
|
"device_class": attributes.and_then(|value| value.get("device_class")).and_then(Value::as_str).unwrap_or_default(),
|
|
}))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
entities.sort_by(|a, b| {
|
|
a.get("entity_id")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or_default()
|
|
.cmp(b.get("entity_id").and_then(Value::as_str).unwrap_or_default())
|
|
});
|
|
Ok(Json(json!({
|
|
"configured": true,
|
|
"entities": entities,
|
|
})))
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct HaEntityRequest {
|
|
entity_id: String,
|
|
}
|
|
|
|
async fn inspect_home_assistant_entity(
|
|
State(state): State<AppState>,
|
|
Json(input): Json<HaEntityRequest>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
let settings = state.settings.read().await.clone();
|
|
let entity_id =
|
|
home_assistant::resolve_entity_id(&settings.home_assistant, Some(input.entity_id.as_str()))
|
|
.filter(|value| !value.trim().is_empty())
|
|
.ok_or_else(|| AppError::BadRequest("Home Assistant entity_id is required".into()))?;
|
|
let payload = home_assistant::read_entity(
|
|
&state.http,
|
|
&settings.home_assistant,
|
|
Some(entity_id.as_str()),
|
|
)
|
|
.await
|
|
.map_err(map_home_assistant_integration_error)?;
|
|
let raw_state = payload
|
|
.get("state")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or_default()
|
|
.to_string();
|
|
let available = !matches!(raw_state.as_str(), "unknown" | "unavailable" | "");
|
|
Ok(Json(json!({
|
|
"ok": true,
|
|
"entity_id": entity_id,
|
|
"state": raw_state,
|
|
"available": available,
|
|
"attributes": payload.get("attributes").cloned().unwrap_or_else(|| json!({})),
|
|
"last_changed": payload.get("last_changed").cloned().unwrap_or(Value::Null),
|
|
"last_updated": payload.get("last_updated").cloned().unwrap_or(Value::Null),
|
|
})))
|
|
}
|
|
|
|
async fn test_notifications(
|
|
State(state): State<AppState>,
|
|
Json(mut input): Json<NotificationSettings>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
let old = state.settings.read().await.notifications.clone();
|
|
if input.pushover_app_token.trim().is_empty() {
|
|
input.pushover_app_token = old.pushover_app_token;
|
|
}
|
|
if input.pushover_user_key.trim().is_empty() {
|
|
input.pushover_user_key = old.pushover_user_key;
|
|
}
|
|
if input.slack_webhook_url.trim().is_empty() {
|
|
input.slack_webhook_url = old.slack_webhook_url;
|
|
}
|
|
if input.discord_webhook_url.trim().is_empty() {
|
|
input.discord_webhook_url = old.discord_webhook_url;
|
|
}
|
|
notifications::test(&state, input)
|
|
.await
|
|
.map_err(map_notification_test_error)?;
|
|
Ok(Json(json!({"ok": true})))
|
|
}
|
|
|
|
async fn list_home_assistant_energy_sensors(
|
|
State(state): State<AppState>,
|
|
) -> Result<Json<Value>, AppError> {
|
|
let settings = state.settings.read().await.home_assistant.clone();
|
|
if !home_assistant::token_configured(&settings)
|
|
|| home_assistant::effective_url(&settings).trim().is_empty()
|
|
{
|
|
return Ok(Json(json!({
|
|
"configured": false,
|
|
"sensors": [],
|
|
})));
|
|
}
|
|
let entities = home_assistant::list_entities(&state.http, &settings)
|
|
.await
|
|
.map_err(|error| AppError::Dependency(error.to_string()))?;
|
|
let sensors = entities
|
|
.into_iter()
|
|
.filter_map(|entity| {
|
|
let attributes = entity.get("attributes")?.as_object()?;
|
|
let device_class = attributes.get("device_class")?.as_str()?;
|
|
let state_class = attributes.get("state_class")?.as_str()?;
|
|
let unit = attributes.get("unit_of_measurement")?.as_str()?;
|
|
if device_class != "energy"
|
|
|| !matches!(state_class, "total" | "total_increasing")
|
|
|| !matches!(unit.to_ascii_lowercase().as_str(), "wh" | "kwh")
|
|
{
|
|
return None;
|
|
}
|
|
Some(json!({
|
|
"entity_id": entity.get("entity_id").and_then(Value::as_str).unwrap_or_default(),
|
|
"name": attributes.get("friendly_name").and_then(Value::as_str).unwrap_or_default(),
|
|
"state": entity.get("state").and_then(Value::as_str).unwrap_or_default(),
|
|
"unit": unit,
|
|
"device_class": device_class,
|
|
"state_class": state_class,
|
|
}))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
Ok(Json(json!({
|
|
"configured": true,
|
|
"sensors": sensors,
|
|
})))
|
|
}
|