363 lines
16 KiB
Rust
363 lines
16 KiB
Rust
use crate::models::{
|
|
DebugSettings, GreeCloudSettings, HomeAssistantSettings, InfluxDbSettings, NightModeSettings,
|
|
NotificationSettings, RuntimeSettings,
|
|
};
|
|
use anyhow::{Context, Result};
|
|
use clap::Parser;
|
|
use std::{env, net::SocketAddr, path::PathBuf};
|
|
|
|
#[derive(Debug, Clone, Parser)]
|
|
#[command(author, version, about)]
|
|
pub struct Config {
|
|
#[arg(long, env = "GREE_CONTROLLER_BIND", default_value = "0.0.0.0:8787")]
|
|
pub bind: SocketAddr,
|
|
#[arg(
|
|
long,
|
|
env = "GREE_CONTROLLER_DATABASE",
|
|
default_value = "./data/gree-controller.db"
|
|
)]
|
|
pub database: PathBuf,
|
|
#[arg(long, env = "GREE_CONTROLLER_APP_TOKEN", default_value = "")]
|
|
pub app_token: String,
|
|
#[arg(long, env = "GREE_CONTROLLER_BASE_PATH", default_value = "")]
|
|
pub base_path: String,
|
|
#[arg(
|
|
long,
|
|
env = "GREE_CONTROLLER_PUBLIC_CHART_BASE_URL",
|
|
default_value = ""
|
|
)]
|
|
pub public_chart_base_url: String,
|
|
#[arg(long, env = "GREE_CONTROLLER_SIMULATE", default_value_t = false)]
|
|
pub simulate: bool,
|
|
#[arg(long, env = "GREE_CONTROLLER_AUTO_SEED", default_value_t = false)]
|
|
pub auto_seed: bool,
|
|
#[arg(
|
|
long,
|
|
env = "GREE_CONTROLLER_POLL_INTERVAL_SECONDS",
|
|
default_value_t = 15
|
|
)]
|
|
pub poll_interval_seconds: u64,
|
|
#[arg(
|
|
long,
|
|
env = "GREE_CONTROLLER_ZONE_INTERVAL_SECONDS",
|
|
default_value_t = 5
|
|
)]
|
|
pub zone_interval_seconds: u64,
|
|
#[arg(
|
|
long,
|
|
env = "GREE_CONTROLLER_DISCOVERY_TIMEOUT_MS",
|
|
default_value_t = 3000
|
|
)]
|
|
pub discovery_timeout_ms: u64,
|
|
#[arg(
|
|
long,
|
|
env = "GREE_CONTROLLER_DISCOVERY_BROADCAST",
|
|
default_value = "255.255.255.255:7000"
|
|
)]
|
|
pub discovery_broadcast: String,
|
|
#[arg(long, env = "GREE_CONTROLLER_GREE_INTERFACE", default_value = "")]
|
|
pub gree_interface: String,
|
|
#[arg(long, env = "GREE_CONTROLLER_ID", default_value = "gree-controller")]
|
|
pub controller_id: String,
|
|
}
|
|
|
|
impl Config {
|
|
pub fn load() -> Result<Self> {
|
|
dotenvy::dotenv().ok();
|
|
let mut config = Self::parse();
|
|
config.base_path = normalize_base_path(&config.base_path)?;
|
|
config.public_chart_base_url =
|
|
normalize_public_chart_base_url(&config.public_chart_base_url)?;
|
|
if let Some(parent) = config.database.parent() {
|
|
std::fs::create_dir_all(parent).with_context(|| {
|
|
format!("cannot create database directory {}", parent.display())
|
|
})?;
|
|
}
|
|
Ok(config)
|
|
}
|
|
|
|
pub fn runtime_defaults(&self) -> RuntimeSettings {
|
|
RuntimeSettings {
|
|
controller_id: self.controller_id.clone(),
|
|
simulator_enabled: self.simulate,
|
|
poll_interval_seconds: self.poll_interval_seconds.max(2),
|
|
zone_interval_seconds: self.zone_interval_seconds.max(2),
|
|
discovery_timeout_ms: self.discovery_timeout_ms.clamp(300, 30_000),
|
|
discovery_broadcast: self.discovery_broadcast.clone(),
|
|
ping_metrics_enabled: env_bool("GREE_CONTROLLER_PING_METRICS_ENABLED").unwrap_or(true),
|
|
ping_interval_seconds: env_u64("GREE_CONTROLLER_PING_INTERVAL_SECONDS")
|
|
.unwrap_or(60)
|
|
.clamp(10, 3600),
|
|
ping_sample_count: env_u32("GREE_CONTROLLER_PING_SAMPLE_COUNT")
|
|
.unwrap_or(3)
|
|
.clamp(1, 10),
|
|
house_mode: env::var("GREE_CONTROLLER_HOUSE_MODE").unwrap_or_else(|_| "cool".into()),
|
|
control_strategy: "setpoint".into(),
|
|
outdoor_assist_enabled: env_bool("GREE_CONTROLLER_OUTDOOR_ASSIST_ENABLED")
|
|
.unwrap_or(true),
|
|
history_retention_days: env_u32("GREE_CONTROLLER_HISTORY_RETENTION_DAYS")
|
|
.unwrap_or(30)
|
|
.clamp(1, 3650),
|
|
history_compaction_enabled: env_bool("GREE_CONTROLLER_HISTORY_COMPACTION_ENABLED")
|
|
.unwrap_or(true),
|
|
event_log_retention_days: env_u32("GREE_CONTROLLER_EVENT_LOG_RETENTION_DAYS")
|
|
.unwrap_or(30)
|
|
.clamp(1, 3650),
|
|
suppress_device_beep: env_bool("GREE_CONTROLLER_SUPPRESS_DEVICE_BEEP").unwrap_or(false),
|
|
compressor_protection_enabled: env_bool(
|
|
"GREE_CONTROLLER_COMPRESSOR_PROTECTION_ENABLED",
|
|
)
|
|
.unwrap_or(true),
|
|
compressor_protection_seconds: env_u64("GREE_CONTROLLER_COMPRESSOR_PROTECTION_SECONDS")
|
|
.unwrap_or(180)
|
|
.clamp(30, 1800),
|
|
influxdb: influx_settings_from_env(),
|
|
debug: DebugSettings {
|
|
overlay_enabled: env_bool("GREE_CONTROLLER_DEBUG_OVERLAY").unwrap_or(false),
|
|
gree_frames: env_bool("GREE_CONTROLLER_DEBUG_GREE_FRAMES").unwrap_or(false),
|
|
cloud_requests: env_bool("GREE_CONTROLLER_DEBUG_CLOUD_REQUESTS").unwrap_or(false),
|
|
cloud_mqtt: env_bool("GREE_CONTROLLER_DEBUG_CLOUD_MQTT").unwrap_or(false),
|
|
},
|
|
notifications: NotificationSettings::default(),
|
|
gree_cloud: GreeCloudSettings::default(),
|
|
night_mode: NightModeSettings {
|
|
enabled: env_bool("GREE_CONTROLLER_NIGHT_MODE_ENABLED").unwrap_or(false),
|
|
start_time: env::var("GREE_CONTROLLER_NIGHT_MODE_START")
|
|
.unwrap_or_else(|_| "22:00".into()),
|
|
end_time: env::var("GREE_CONTROLLER_NIGHT_MODE_END")
|
|
.unwrap_or_else(|_| "06:00".into()),
|
|
max_fan_speed: env_u8("GREE_CONTROLLER_NIGHT_MODE_MAX_FAN_SPEED")
|
|
.unwrap_or(1)
|
|
.clamp(1, 5),
|
|
force_quiet: env_bool("GREE_CONTROLLER_NIGHT_MODE_FORCE_QUIET").unwrap_or(true),
|
|
use_native_sleep: env_bool("GREE_CONTROLLER_NIGHT_MODE_NATIVE_SLEEP")
|
|
.unwrap_or(true),
|
|
},
|
|
home_assistant: HomeAssistantSettings {
|
|
url: env::var("HA_URL").unwrap_or_default(),
|
|
token: env::var("HA_TOKEN").unwrap_or_default(),
|
|
outdoor_entity_id: env::var("HA_OUTDOOR_ENTITY_ID").unwrap_or_default(),
|
|
sensor_stale_after_seconds: env_u64("HA_SENSOR_STALE_AFTER_SECONDS")
|
|
.unwrap_or(300)
|
|
.clamp(30, 86_400),
|
|
allow_invalid_tls: env::var("HA_ALLOW_INVALID_TLS")
|
|
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
|
|
.unwrap_or(false),
|
|
manual_auth_override: false,
|
|
sensor_aliases: Default::default(),
|
|
flow_inputs: Vec::new(),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Environment values explicitly supplied by the service override persisted runtime values.
|
|
pub fn apply_runtime_env_overrides(&self, settings: &mut RuntimeSettings) {
|
|
if let Some(value) = env_bool("GREE_CONTROLLER_PING_METRICS_ENABLED") {
|
|
settings.ping_metrics_enabled = value;
|
|
}
|
|
if let Some(value) = env_u64("GREE_CONTROLLER_PING_INTERVAL_SECONDS") {
|
|
settings.ping_interval_seconds = value.clamp(10, 3600);
|
|
}
|
|
if let Some(value) = env_u32("GREE_CONTROLLER_PING_SAMPLE_COUNT") {
|
|
settings.ping_sample_count = value.clamp(1, 10);
|
|
}
|
|
if env::var_os("GREE_CONTROLLER_HISTORY_RETENTION_DAYS").is_some() {
|
|
settings.history_retention_days = env_u32("GREE_CONTROLLER_HISTORY_RETENTION_DAYS")
|
|
.unwrap_or(settings.history_retention_days)
|
|
.clamp(1, 3650);
|
|
}
|
|
if let Some(value) = env_bool("GREE_CONTROLLER_HISTORY_COMPACTION_ENABLED") {
|
|
settings.history_compaction_enabled = value;
|
|
}
|
|
if env::var_os("GREE_CONTROLLER_EVENT_LOG_RETENTION_DAYS").is_some() {
|
|
settings.event_log_retention_days = env_u32("GREE_CONTROLLER_EVENT_LOG_RETENTION_DAYS")
|
|
.unwrap_or(settings.event_log_retention_days)
|
|
.clamp(1, 3650);
|
|
}
|
|
if let Some(value) = env_bool("GREE_CONTROLLER_SUPPRESS_DEVICE_BEEP") {
|
|
settings.suppress_device_beep = value;
|
|
}
|
|
if let Some(value) = env_bool("GREE_CONTROLLER_COMPRESSOR_PROTECTION_ENABLED") {
|
|
settings.compressor_protection_enabled = value;
|
|
}
|
|
if let Some(value) = env_u64("GREE_CONTROLLER_COMPRESSOR_PROTECTION_SECONDS") {
|
|
settings.compressor_protection_seconds = value.clamp(30, 1800);
|
|
}
|
|
if let Some(value) = env_bool("GREE_CONTROLLER_DEBUG_OVERLAY") {
|
|
settings.debug.overlay_enabled = value;
|
|
}
|
|
if let Some(value) = env_bool("GREE_CONTROLLER_DEBUG_GREE_FRAMES") {
|
|
settings.debug.gree_frames = value;
|
|
}
|
|
if let Some(value) = env_bool("GREE_CONTROLLER_DEBUG_CLOUD_REQUESTS") {
|
|
settings.debug.cloud_requests = value;
|
|
}
|
|
if let Some(value) = env_bool("GREE_CONTROLLER_DEBUG_CLOUD_MQTT") {
|
|
settings.debug.cloud_mqtt = value;
|
|
}
|
|
if let Some(value) = env_bool("GREE_CONTROLLER_NIGHT_MODE_ENABLED") {
|
|
settings.night_mode.enabled = value;
|
|
}
|
|
if let Ok(value) = env::var("GREE_CONTROLLER_NIGHT_MODE_START") {
|
|
if !value.trim().is_empty() {
|
|
settings.night_mode.start_time = value;
|
|
}
|
|
}
|
|
if let Ok(value) = env::var("GREE_CONTROLLER_NIGHT_MODE_END") {
|
|
if !value.trim().is_empty() {
|
|
settings.night_mode.end_time = value;
|
|
}
|
|
}
|
|
if let Some(value) = env_u8("GREE_CONTROLLER_NIGHT_MODE_MAX_FAN_SPEED") {
|
|
settings.night_mode.max_fan_speed = value.clamp(1, 5);
|
|
}
|
|
if let Some(value) = env_bool("GREE_CONTROLLER_NIGHT_MODE_FORCE_QUIET") {
|
|
settings.night_mode.force_quiet = value;
|
|
}
|
|
if let Some(value) = env_bool("GREE_CONTROLLER_NIGHT_MODE_NATIVE_SLEEP") {
|
|
settings.night_mode.use_native_sleep = value;
|
|
}
|
|
if let Some(value) = env_u64("HA_SENSOR_STALE_AFTER_SECONDS") {
|
|
settings.home_assistant.sensor_stale_after_seconds = value.clamp(30, 86_400);
|
|
}
|
|
|
|
let influx_env_present = [
|
|
"GREE_CONTROLLER_INFLUX_ENABLED",
|
|
"GREE_CONTROLLER_INFLUX_VERSION",
|
|
"GREE_CONTROLLER_INFLUX_URL",
|
|
"GREE_CONTROLLER_INFLUX_DATABASE",
|
|
"GREE_CONTROLLER_INFLUX_USERNAME",
|
|
"GREE_CONTROLLER_INFLUX_PASSWORD",
|
|
"GREE_CONTROLLER_INFLUX_ORG",
|
|
"GREE_CONTROLLER_INFLUX_BUCKET",
|
|
"GREE_CONTROLLER_INFLUX_TOKEN",
|
|
"GREE_CONTROLLER_INFLUX_THRESHOLD_DAYS",
|
|
"INFLUXDB_URL",
|
|
"INFLUXDB_DATABASE",
|
|
"INFLUXDB_USERNAME",
|
|
"INFLUXDB_PASSWORD",
|
|
"INFLUXDB_TOKEN",
|
|
"INFLUXDB_ORG",
|
|
"INFLUXDB_BUCKET",
|
|
]
|
|
.iter()
|
|
.any(|name| env::var_os(name).is_some());
|
|
if influx_env_present {
|
|
let env_settings = influx_settings_from_env();
|
|
if env::var_os("GREE_CONTROLLER_INFLUX_ENABLED").is_some() {
|
|
settings.influxdb.enabled = env_settings.enabled;
|
|
} else if first_env(&["GREE_CONTROLLER_INFLUX_URL", "INFLUXDB_URL"]).is_some() {
|
|
settings.influxdb.enabled = true;
|
|
}
|
|
if first_env(&["GREE_CONTROLLER_INFLUX_VERSION"]).is_some() {
|
|
settings.influxdb.version = env_settings.version;
|
|
}
|
|
if first_env(&["GREE_CONTROLLER_INFLUX_URL", "INFLUXDB_URL"]).is_some() {
|
|
settings.influxdb.url = env_settings.url;
|
|
}
|
|
if first_env(&["GREE_CONTROLLER_INFLUX_DATABASE", "INFLUXDB_DATABASE"]).is_some() {
|
|
settings.influxdb.database = env_settings.database;
|
|
}
|
|
if first_env(&["GREE_CONTROLLER_INFLUX_USERNAME", "INFLUXDB_USERNAME"]).is_some() {
|
|
settings.influxdb.username = env_settings.username;
|
|
}
|
|
if first_env(&["GREE_CONTROLLER_INFLUX_PASSWORD", "INFLUXDB_PASSWORD"]).is_some() {
|
|
settings.influxdb.password = env_settings.password;
|
|
}
|
|
if first_env(&["GREE_CONTROLLER_INFLUX_ORG", "INFLUXDB_ORG"]).is_some() {
|
|
settings.influxdb.org = env_settings.org;
|
|
}
|
|
if first_env(&["GREE_CONTROLLER_INFLUX_BUCKET", "INFLUXDB_BUCKET"]).is_some() {
|
|
settings.influxdb.bucket = env_settings.bucket;
|
|
}
|
|
if first_env(&["GREE_CONTROLLER_INFLUX_TOKEN", "INFLUXDB_TOKEN"]).is_some() {
|
|
settings.influxdb.token = env_settings.token;
|
|
}
|
|
if env::var_os("GREE_CONTROLLER_INFLUX_THRESHOLD_DAYS").is_some() {
|
|
settings.influxdb.history_threshold_days = env_settings.history_threshold_days;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn normalize_base_path(value: &str) -> Result<String> {
|
|
let value = value.trim();
|
|
if value.is_empty() || value == "/" {
|
|
return Ok(String::new());
|
|
}
|
|
if value.contains('?')
|
|
|| value.contains('#')
|
|
|| value.split('/').any(|part| matches!(part, "." | ".."))
|
|
{
|
|
anyhow::bail!("GREE_CONTROLLER_BASE_PATH must be a simple URL path without '.', '..', query or fragment");
|
|
}
|
|
Ok(format!("/{}", value.trim_matches('/')))
|
|
}
|
|
|
|
fn normalize_public_chart_base_url(value: &str) -> Result<String> {
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
return Ok(String::new());
|
|
}
|
|
|
|
let parsed = url::Url::parse(value)
|
|
.with_context(|| "GREE_CONTROLLER_PUBLIC_CHART_BASE_URL must be an absolute HTTP(S) URL")?;
|
|
if !matches!(parsed.scheme(), "http" | "https")
|
|
|| parsed.host_str().is_none()
|
|
|| !parsed.username().is_empty()
|
|
|| parsed.password().is_some()
|
|
|| parsed.query().is_some()
|
|
|| parsed.fragment().is_some()
|
|
{
|
|
anyhow::bail!("GREE_CONTROLLER_PUBLIC_CHART_BASE_URL must be an absolute HTTP(S) URL without credentials, query or fragment");
|
|
}
|
|
|
|
Ok(value.trim_end_matches('/').to_string())
|
|
}
|
|
|
|
fn env_bool(name: &str) -> Option<bool> {
|
|
env::var(name)
|
|
.ok()
|
|
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
|
|
}
|
|
|
|
fn env_u32(name: &str) -> Option<u32> {
|
|
env::var(name).ok()?.parse().ok()
|
|
}
|
|
fn env_u64(name: &str) -> Option<u64> {
|
|
env::var(name).ok()?.parse().ok()
|
|
}
|
|
fn env_u8(name: &str) -> Option<u8> {
|
|
env::var(name).ok()?.parse().ok()
|
|
}
|
|
|
|
fn first_env(names: &[&str]) -> Option<String> {
|
|
names.iter().find_map(|name| {
|
|
let value = env::var(name).ok()?;
|
|
(!value.trim().is_empty()).then_some(value)
|
|
})
|
|
}
|
|
|
|
fn influx_settings_from_env() -> InfluxDbSettings {
|
|
let mut settings = InfluxDbSettings::default();
|
|
settings.version = first_env(&["GREE_CONTROLLER_INFLUX_VERSION"]).unwrap_or_else(|| "2".into());
|
|
settings.url = first_env(&["GREE_CONTROLLER_INFLUX_URL", "INFLUXDB_URL"]).unwrap_or_default();
|
|
settings.enabled =
|
|
env_bool("GREE_CONTROLLER_INFLUX_ENABLED").unwrap_or(!settings.url.is_empty());
|
|
settings.database = first_env(&["GREE_CONTROLLER_INFLUX_DATABASE", "INFLUXDB_DATABASE"])
|
|
.unwrap_or_else(|| "gree_controller".into());
|
|
settings.username =
|
|
first_env(&["GREE_CONTROLLER_INFLUX_USERNAME", "INFLUXDB_USERNAME"]).unwrap_or_default();
|
|
settings.password =
|
|
first_env(&["GREE_CONTROLLER_INFLUX_PASSWORD", "INFLUXDB_PASSWORD"]).unwrap_or_default();
|
|
settings.org = first_env(&["GREE_CONTROLLER_INFLUX_ORG", "INFLUXDB_ORG"]).unwrap_or_default();
|
|
settings.bucket = first_env(&["GREE_CONTROLLER_INFLUX_BUCKET", "INFLUXDB_BUCKET"])
|
|
.unwrap_or_else(|| "gree_controller".into());
|
|
settings.token =
|
|
first_env(&["GREE_CONTROLLER_INFLUX_TOKEN", "INFLUXDB_TOKEN"]).unwrap_or_default();
|
|
settings.history_threshold_days = env_u32("GREE_CONTROLLER_INFLUX_THRESHOLD_DAYS")
|
|
.unwrap_or(30)
|
|
.clamp(1, 3650);
|
|
settings
|
|
}
|