v0.14.12
This commit is contained in:
@@ -27,7 +27,7 @@ async fn export_configuration(
|
||||
|
||||
fn validate_configuration_header(export: &ConfigurationExport) -> Result<(), AppError> {
|
||||
if export.format_version != 3 {
|
||||
return Err(AppError::BadRequest("unsupported configuration export version; version 3 is required by GREE Controller 0.14.11".into()));
|
||||
return Err(AppError::BadRequest("unsupported configuration export version; version 3 is required by GREE Controller 0.14.12".into()));
|
||||
}
|
||||
if export.settings.control_strategy != "setpoint" {
|
||||
return Err(AppError::BadRequest(
|
||||
|
||||
@@ -49,9 +49,10 @@ fn public_settings(settings: &RuntimeSettings) -> Value {
|
||||
"history_threshold_days": settings.influxdb.history_threshold_days,
|
||||
},
|
||||
"home_assistant": {
|
||||
"url": settings.home_assistant.url,
|
||||
"url": home_assistant::effective_url(&settings.home_assistant),
|
||||
"auth_mode": home_assistant::auth_mode(),
|
||||
"token": "",
|
||||
"token_configured": !settings.home_assistant.token.trim().is_empty(),
|
||||
"token_configured": home_assistant::token_configured(&settings.home_assistant),
|
||||
"outdoor_entity_id": settings.home_assistant.outdoor_entity_id,
|
||||
"sensor_stale_after_seconds": settings.home_assistant.sensor_stale_after_seconds,
|
||||
"allow_invalid_tls": settings.home_assistant.allow_invalid_tls,
|
||||
|
||||
+16
-6
@@ -78,8 +78,9 @@ fn notification_settings(settings: &RuntimeSettings) -> NotificationSettingsView
|
||||
|
||||
fn home_assistant_settings(settings: &RuntimeSettings) -> HomeAssistantSettingsView {
|
||||
HomeAssistantSettingsView {
|
||||
url: settings.home_assistant.url.clone(),
|
||||
token_configured: !settings.home_assistant.token.trim().is_empty(),
|
||||
url: home_assistant::effective_url(&settings.home_assistant),
|
||||
auth_mode: home_assistant::auth_mode().to_string(),
|
||||
token_configured: home_assistant::token_configured(&settings.home_assistant),
|
||||
outdoor_entity_id: settings.home_assistant.outdoor_entity_id.clone(),
|
||||
sensor_stale_after_seconds: settings.home_assistant.sensor_stale_after_seconds,
|
||||
allow_invalid_tls: settings.home_assistant.allow_invalid_tls,
|
||||
@@ -693,8 +694,13 @@ fn apply_home_assistant_update(
|
||||
current: &HomeAssistantSettings,
|
||||
input: HomeAssistantSettingsUpdate,
|
||||
) -> HomeAssistantSettings {
|
||||
let supervisor_managed = home_assistant::uses_supervisor_auth();
|
||||
let mut next = HomeAssistantSettings {
|
||||
url: input.url,
|
||||
url: if supervisor_managed {
|
||||
current.url.clone()
|
||||
} else {
|
||||
input.url
|
||||
},
|
||||
token: current.token.clone(),
|
||||
outdoor_entity_id: input.outdoor_entity_id,
|
||||
sensor_stale_after_seconds: input.sensor_stale_after_seconds.clamp(30, 86_400),
|
||||
@@ -702,8 +708,10 @@ fn apply_home_assistant_update(
|
||||
sensor_aliases: input.sensor_aliases,
|
||||
flow_inputs: input.flow_inputs,
|
||||
};
|
||||
if let Some(token) = input.token {
|
||||
next.token = token;
|
||||
if !supervisor_managed {
|
||||
if let Some(token) = input.token {
|
||||
next.token = token;
|
||||
}
|
||||
}
|
||||
next
|
||||
}
|
||||
@@ -720,7 +728,9 @@ async fn update_home_assistant_settings(
|
||||
normalize_sensor_aliases(&mut next);
|
||||
validate_flow_shared_inputs(&mut next, &state)?;
|
||||
canonicalize_home_assistant_entities(&mut next);
|
||||
validate_home_assistant_url(&next)?;
|
||||
if !home_assistant::uses_supervisor_auth() {
|
||||
validate_home_assistant_url(&next)?;
|
||||
}
|
||||
settings.home_assistant = next.clone();
|
||||
settings.outdoor_assist_enabled = input.outdoor_assist_enabled;
|
||||
state.db.save_runtime_settings(&settings)?;
|
||||
|
||||
+74
-56
@@ -1,9 +1,72 @@
|
||||
use crate::models::HomeAssistantSettings;
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
use std::{env, time::Duration};
|
||||
use url::Url;
|
||||
|
||||
const SUPERVISOR_AUTH_ENV: &str = "GREE_CONTROLLER_HA_AUTH";
|
||||
const SUPERVISOR_AUTH_MODE: &str = "supervisor";
|
||||
const SUPERVISOR_TOKEN_ENV: &str = "SUPERVISOR_TOKEN";
|
||||
const SUPERVISOR_BASE_URL: &str = "http://supervisor/core/";
|
||||
|
||||
pub fn uses_supervisor_auth() -> bool {
|
||||
env::var(SUPERVISOR_AUTH_ENV)
|
||||
.map(|value| value.trim().eq_ignore_ascii_case(SUPERVISOR_AUTH_MODE))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn auth_mode() -> &'static str {
|
||||
if uses_supervisor_auth() {
|
||||
SUPERVISOR_AUTH_MODE
|
||||
} else {
|
||||
"manual"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn effective_url(settings: &HomeAssistantSettings) -> String {
|
||||
if uses_supervisor_auth() {
|
||||
SUPERVISOR_BASE_URL.to_string()
|
||||
} else {
|
||||
settings.url.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token_configured(settings: &HomeAssistantSettings) -> bool {
|
||||
if uses_supervisor_auth() {
|
||||
env::var(SUPERVISOR_TOKEN_ENV)
|
||||
.map(|token| !token.trim().is_empty())
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
!settings.token.trim().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn connection(settings: &HomeAssistantSettings) -> Result<(Url, String)> {
|
||||
let (raw_url, token) = if uses_supervisor_auth() {
|
||||
let token = env::var(SUPERVISOR_TOKEN_ENV)
|
||||
.context("Home Assistant Supervisor token is not available")?;
|
||||
(SUPERVISOR_BASE_URL.to_string(), token)
|
||||
} else {
|
||||
(settings.url.clone(), settings.token.clone())
|
||||
};
|
||||
|
||||
if raw_url.trim().is_empty() {
|
||||
bail!("Home Assistant URL is not configured")
|
||||
}
|
||||
if token.trim().is_empty() {
|
||||
if uses_supervisor_auth() {
|
||||
bail!("Home Assistant Supervisor token is not available")
|
||||
}
|
||||
bail!("Home Assistant token is not configured")
|
||||
}
|
||||
|
||||
let base = Url::parse(raw_url.trim()).context("invalid Home Assistant URL")?;
|
||||
if !matches!(base.scheme(), "http" | "https") {
|
||||
bail!("Home Assistant URL must use http or https")
|
||||
}
|
||||
Ok((base, token))
|
||||
}
|
||||
|
||||
fn request_client(
|
||||
default_client: &reqwest::Client,
|
||||
settings: &HomeAssistantSettings,
|
||||
@@ -48,20 +111,11 @@ pub async fn test_connection(
|
||||
default_client: &reqwest::Client,
|
||||
settings: &HomeAssistantSettings,
|
||||
) -> Result<()> {
|
||||
if settings.url.trim().is_empty() {
|
||||
bail!("Home Assistant URL is not configured")
|
||||
}
|
||||
if settings.token.trim().is_empty() {
|
||||
bail!("Home Assistant token is not configured")
|
||||
}
|
||||
let mut base = Url::parse(settings.url.trim()).context("invalid Home Assistant URL")?;
|
||||
if !matches!(base.scheme(), "http" | "https") {
|
||||
bail!("Home Assistant URL must use http or https")
|
||||
}
|
||||
let (mut base, token) = connection(settings)?;
|
||||
base = base.join("api/").context("cannot build Home Assistant API URL")?;
|
||||
let response = request_client(default_client, settings)?
|
||||
.get(base)
|
||||
.bearer_auth(settings.token.trim())
|
||||
.bearer_auth(token.trim())
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
.await
|
||||
@@ -83,19 +137,10 @@ pub async fn read_temperature(
|
||||
entity_override: Option<&str>,
|
||||
stale_after_seconds: Option<u64>,
|
||||
) -> Result<f64> {
|
||||
if settings.url.trim().is_empty() {
|
||||
bail!("Home Assistant URL is not configured")
|
||||
}
|
||||
if settings.token.trim().is_empty() {
|
||||
bail!("Home Assistant token is not configured")
|
||||
}
|
||||
let (mut base, token) = connection(settings)?;
|
||||
let entity = resolve_entity_id(settings, entity_override)
|
||||
.ok_or_else(|| anyhow!("Home Assistant entity_id is not configured"))?;
|
||||
|
||||
let mut base = Url::parse(settings.url.trim()).context("invalid Home Assistant URL")?;
|
||||
if !matches!(base.scheme(), "http" | "https") {
|
||||
bail!("Home Assistant URL must use http or https")
|
||||
}
|
||||
let path = format!("api/states/{}", entity.trim_start_matches('/'));
|
||||
base = base
|
||||
.join(&path)
|
||||
@@ -104,7 +149,7 @@ pub async fn read_temperature(
|
||||
let client = request_client(default_client, settings)?;
|
||||
let response = client
|
||||
.get(base)
|
||||
.bearer_auth(settings.token.trim())
|
||||
.bearer_auth(token.trim())
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
.await
|
||||
@@ -195,25 +240,16 @@ pub async fn read_entity(
|
||||
settings: &HomeAssistantSettings,
|
||||
entity_override: Option<&str>,
|
||||
) -> Result<Value> {
|
||||
if settings.url.trim().is_empty() {
|
||||
bail!("Home Assistant URL is not configured")
|
||||
}
|
||||
if settings.token.trim().is_empty() {
|
||||
bail!("Home Assistant token is not configured")
|
||||
}
|
||||
let (mut base, token) = connection(settings)?;
|
||||
let entity = resolve_entity_id(settings, entity_override)
|
||||
.ok_or_else(|| anyhow!("Home Assistant entity_id is not configured"))?;
|
||||
let mut base = Url::parse(settings.url.trim()).context("invalid Home Assistant URL")?;
|
||||
if !matches!(base.scheme(), "http" | "https") {
|
||||
bail!("Home Assistant URL must use http or https")
|
||||
}
|
||||
base = base
|
||||
.join(&format!("api/states/{}", entity.trim_start_matches('/')))
|
||||
.context("cannot build Home Assistant API URL")?;
|
||||
let client = request_client(default_client, settings)?;
|
||||
let response = client
|
||||
.get(base)
|
||||
.bearer_auth(settings.token.trim())
|
||||
.bearer_auth(token.trim())
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
.await
|
||||
@@ -234,20 +270,11 @@ pub async fn list_entities(
|
||||
default_client: &reqwest::Client,
|
||||
settings: &HomeAssistantSettings,
|
||||
) -> Result<Vec<Value>> {
|
||||
if settings.url.trim().is_empty() {
|
||||
bail!("Home Assistant URL is not configured")
|
||||
}
|
||||
if settings.token.trim().is_empty() {
|
||||
bail!("Home Assistant token is not configured")
|
||||
}
|
||||
let mut base = Url::parse(settings.url.trim()).context("invalid Home Assistant URL")?;
|
||||
if !matches!(base.scheme(), "http" | "https") {
|
||||
bail!("Home Assistant URL must use http or https")
|
||||
}
|
||||
let (mut base, token) = connection(settings)?;
|
||||
base = base.join("api/states").context("cannot build Home Assistant API URL")?;
|
||||
let response = request_client(default_client, settings)?
|
||||
.get(base)
|
||||
.bearer_auth(settings.token.trim())
|
||||
.bearer_auth(token.trim())
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
.await
|
||||
@@ -292,19 +319,10 @@ pub async fn call_service(
|
||||
entity_id: Option<&str>,
|
||||
data: &Value,
|
||||
) -> Result<Value> {
|
||||
if settings.url.trim().is_empty() {
|
||||
bail!("Home Assistant URL is not configured")
|
||||
}
|
||||
if settings.token.trim().is_empty() {
|
||||
bail!("Home Assistant token is not configured")
|
||||
}
|
||||
let (mut base, token) = connection(settings)?;
|
||||
if domain.trim().is_empty() || service.trim().is_empty() {
|
||||
bail!("Home Assistant domain/service is required")
|
||||
}
|
||||
let mut base = Url::parse(settings.url.trim()).context("invalid Home Assistant URL")?;
|
||||
if !matches!(base.scheme(), "http" | "https") {
|
||||
bail!("Home Assistant URL must use http or https")
|
||||
}
|
||||
base = base
|
||||
.join(&format!(
|
||||
"api/services/{}/{}",
|
||||
@@ -319,7 +337,7 @@ pub async fn call_service(
|
||||
let client = request_client(default_client, settings)?;
|
||||
let response = client
|
||||
.post(base)
|
||||
.bearer_auth(settings.token.trim())
|
||||
.bearer_auth(token.trim())
|
||||
.header("Accept", "application/json")
|
||||
.json(&Value::Object(payload))
|
||||
.send()
|
||||
|
||||
@@ -139,6 +139,7 @@ pub struct HomeAssistantSettingsUpdate {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HomeAssistantSettingsView {
|
||||
pub url: String,
|
||||
pub auth_mode: String,
|
||||
pub token_configured: bool,
|
||||
pub outdoor_entity_id: String,
|
||||
pub sensor_stale_after_seconds: u64,
|
||||
|
||||
Reference in New Issue
Block a user