v0.12.0-preety_code

This commit is contained in:
Mateusz Gruszczyński
2026-09-03 15:33:37 +02:00
parent be67932b47
commit 07be4b85d9
87 changed files with 11691 additions and 3172 deletions
+192 -63
View File
@@ -1,14 +1,21 @@
use std::{env, net::SocketAddr, path::PathBuf};
use crate::models::{
DebugSettings, HomeAssistantSettings, InfluxDbSettings, NightModeSettings,
NotificationSettings, RuntimeSettings,
};
use anyhow::{Context, Result};
use clap::Parser;
use crate::models::{DebugSettings, HomeAssistantSettings, InfluxDbSettings, NightModeSettings, NotificationSettings, RuntimeSettings};
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")]
#[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,
@@ -18,13 +25,29 @@ pub struct Config {
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)]
#[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)]
#[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)]
#[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")]
#[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,
@@ -38,8 +61,9 @@ impl Config {
let mut config = Self::parse();
config.base_path = normalize_base_path(&config.base_path)?;
if let Some(parent) = config.database.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("cannot create database directory {}", parent.display()))?;
std::fs::create_dir_all(parent).with_context(|| {
format!("cannot create database directory {}", parent.display())
})?;
}
Ok(config)
}
@@ -54,13 +78,24 @@ impl Config {
discovery_broadcast: self.discovery_broadcast.clone(),
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),
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),
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),
@@ -69,18 +104,25 @@ impl Config {
notifications: NotificationSettings::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),
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),
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(),
default_entity_id: env::var("HA_ENTITY_ID").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),
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),
@@ -93,32 +135,80 @@ impl Config {
/// Environment values explicitly supplied by the service override persisted runtime values.
pub fn apply_runtime_env_overrides(&self, settings: &mut RuntimeSettings) {
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);
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 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);
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_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);
}
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_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());
"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() {
@@ -126,35 +216,66 @@ impl Config {
} 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; }
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, "." | "..")) {
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 env_bool(name: &str) -> Option<bool> {
env::var(name).ok().map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
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 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| {
@@ -167,13 +288,21 @@ 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.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.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
}