This commit is contained in:
Mateusz Gruszczyński committed 2026-09-14 16:32:28 +02:00
1 parent c3fbc5ccc6
commit 021cbddba5
68 files changed
+7780 -202

No files matched your search

+6
View File
@@ -82,6 +82,12 @@ fn default_history_retention_days() -> u32 {
fn default_event_log_retention_days() -> u32 {
30
}
fn default_cloud_region() -> String {
"Europe".into()
}
fn default_cloud_poll_interval_seconds() -> u64 {
60
}
fn default_influx_version() -> String {
"2".into()
}
+184 -3
View File
@@ -1,8 +1,94 @@
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "snake_case")]
pub enum ConnectionType {
#[default]
Local,
GreeCloud,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum ConnectionStatus {
Online,
Offline,
CloudDisconnected,
AuthenticationError,
#[default]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceCapabilities {
#[serde(default = "default_true")]
pub power: bool,
#[serde(default = "default_true")]
pub mode: bool,
#[serde(default = "default_fan_modes")]
pub fan_modes: Vec<u8>,
#[serde(default = "default_min_temperature")]
pub min_temperature: f64,
#[serde(default = "default_max_temperature")]
pub max_temperature: f64,
#[serde(default = "default_temperature_step")]
pub temperature_step: f64,
#[serde(default = "default_true")]
pub vertical_swing: bool,
#[serde(default = "default_true")]
pub horizontal_swing: bool,
#[serde(default)] pub turbo: bool,
#[serde(default)] pub quiet: bool,
#[serde(default)] pub sleep: bool,
#[serde(default)] pub light: bool,
#[serde(default)] pub health: bool,
#[serde(default)] pub buzzer_control: bool,
#[serde(default)] pub energy_meter: bool,
#[serde(default)] pub compressor_frequency: bool,
}
impl Default for DeviceCapabilities {
fn default() -> Self {
Self {
power: true, mode: true, fan_modes: default_fan_modes(),
min_temperature: 8.0, max_temperature: 30.0, temperature_step: 1.0,
vertical_swing: true, horizontal_swing: true, turbo: false, quiet: false,
sleep: false, light: false, health: false, buzzer_control: false,
energy_meter: false, compressor_frequency: false,
}
}
}
fn default_fan_modes() -> Vec<u8> { vec![0, 1, 2, 3, 4, 5] }
fn default_min_temperature() -> f64 { 8.0 }
fn default_max_temperature() -> f64 { 30.0 }
fn default_temperature_step() -> f64 { 1.0 }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum EnergySourcePreference {
#[default]
Auto,
GreeCloud,
HomeAssistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Device {
pub id: String,
pub mac: String,
pub name: String,
/// Explicit transport. Missing in old JSON records => Local.
#[serde(default)]
pub connection_type: ConnectionType,
#[serde(default)]
pub connection_status: ConnectionStatus,
/// Stable identifier reported by the GREE Cloud account (currently the cloud MAC/CID).
#[serde(default)]
pub cloud_device_id: Option<String>,
#[serde(default)]
pub cloud_parent_mac: Option<String>,
#[serde(default)]
pub cloud_account_id: Option<String>,
pub ip: String,
#[serde(default = "default_port")]
pub port: u16,
@@ -12,7 +98,7 @@ pub struct Device {
pub model: String,
#[serde(default)]
pub firmware: String,
#[serde(default)]
#[serde(default, skip_serializing)]
pub key: Option<String>,
#[serde(default)]
pub cid: Option<String>,
@@ -70,6 +156,16 @@ pub struct Device {
#[serde(default)]
pub supports_sleep: Option<bool>,
#[serde(default)]
pub supports_buzzer_control: Option<bool>,
#[serde(default)]
pub supports_energy_meter: Option<bool>,
#[serde(default)]
pub total_energy_kwh: Option<f64>,
#[serde(default)]
pub compressor_frequency_hz: Option<f64>,
#[serde(default)]
pub last_cloud_sync: Option<DateTime<Utc>>,
#[serde(default)]
pub current_temperature: Option<f64>,
#[serde(default)]
pub outdoor_temperature: Option<f64>,
@@ -87,6 +183,22 @@ pub struct Device {
pub last_error: Option<String>,
#[serde(default)]
pub communication_failures: u8,
/// True while a Cloud command is awaiting confirmation. Local transport keeps its
/// historical synchronous UI behaviour and normally leaves this false.
#[serde(default)]
pub pending_command: bool,
#[serde(default)]
pub capabilities: DeviceCapabilities,
#[serde(default)]
pub energy_source: EnergySourcePreference,
#[serde(default)]
pub ha_energy_entity_id: Option<String>,
#[serde(default)]
pub ha_energy_unit: Option<String>,
#[serde(default)]
pub ha_energy_device_class: Option<String>,
#[serde(default)]
pub ha_energy_state_class: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -98,6 +210,11 @@ impl Device {
id: "sim-salon".into(),
mac: "SIM000000001".into(),
name: "Living Room (simulator)".into(),
connection_type: ConnectionType::Local,
connection_status: ConnectionStatus::Online,
cloud_device_id: None,
cloud_parent_mac: None,
cloud_account_id: None,
ip: "127.0.0.1".into(),
port: 7000,
protocol_version: 1,
@@ -128,6 +245,11 @@ impl Device {
supports_xfan: Some(true),
supports_health: Some(true),
supports_sleep: Some(true),
supports_buzzer_control: Some(true),
supports_energy_meter: None,
total_energy_kwh: None,
compressor_frequency_hz: None,
last_cloud_sync: None,
current_temperature: Some(26.0),
outdoor_temperature: Some(30.0),
temperature_sensor_offset: Some(false),
@@ -136,10 +258,41 @@ impl Device {
last_seen: Some(now),
last_error: None,
communication_failures: 0,
pending_command: false,
capabilities: DeviceCapabilities {
turbo: true, quiet: true, sleep: true, light: true, health: true,
buzzer_control: true, ..DeviceCapabilities::default()
},
energy_source: EnergySourcePreference::Auto,
ha_energy_entity_id: None,
ha_energy_unit: None,
ha_energy_device_class: None,
ha_energy_state_class: None,
created_at: now,
updated_at: now,
}
}
pub fn refresh_capabilities(&mut self) {
self.capabilities.power = true;
self.capabilities.mode = true;
self.capabilities.fan_modes = default_fan_modes();
self.capabilities.min_temperature = 8.0;
self.capabilities.max_temperature = 30.0;
if self.connection_type == ConnectionType::Local {
self.capabilities.vertical_swing = true;
self.capabilities.horizontal_swing = true;
self.capabilities.temperature_step = 1.0;
}
self.capabilities.turbo = self.supports_turbo.unwrap_or(self.connection_type == ConnectionType::Local);
self.capabilities.quiet = self.supports_quiet.unwrap_or(self.connection_type == ConnectionType::Local);
self.capabilities.sleep = self.supports_sleep.unwrap_or(false);
self.capabilities.light = self.supports_light.unwrap_or(self.connection_type == ConnectionType::Local);
self.capabilities.health = self.supports_health.unwrap_or(false);
self.capabilities.buzzer_control = self.supports_buzzer_control.unwrap_or(false);
self.capabilities.energy_meter = self.supports_energy_meter.unwrap_or(false);
self.capabilities.compressor_frequency = self.compressor_frequency_hz.is_some();
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -150,6 +303,11 @@ pub struct DevicePatch {
pub protocol_version: Option<u8>,
pub key: Option<Option<String>>,
pub enabled: Option<bool>,
pub energy_source: Option<EnergySourcePreference>,
pub ha_energy_entity_id: Option<Option<String>>,
pub ha_energy_unit: Option<Option<String>>,
pub ha_energy_device_class: Option<Option<String>>,
pub ha_energy_state_class: Option<Option<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -196,7 +354,10 @@ impl DeviceCommand {
.filter(|value| value.as_str() != device.mode.as_str())
.cloned(),
target_temperature: self.target_temperature.filter(|value| {
value.clamp(8.0, 30.0).round() != device.target_temperature.clamp(8.0, 30.0).round()
let step = device.capabilities.temperature_step.max(0.5);
let requested = (value.clamp(device.capabilities.min_temperature, device.capabilities.max_temperature) / step).round();
let current = (device.target_temperature.clamp(device.capabilities.min_temperature, device.capabilities.max_temperature) / step).round();
requested != current
}),
fan_speed: self
.fan_speed
@@ -225,7 +386,10 @@ impl DeviceCommand {
device.mode = v.clone();
}
if let Some(v) = self.target_temperature {
device.target_temperature = v.clamp(8.0, 30.0).round();
let step = device.capabilities.temperature_step.max(0.5);
let min = device.capabilities.min_temperature;
let max = device.capabilities.max_temperature;
device.target_temperature = (v.clamp(min, max) / step).round() * step;
}
if let Some(v) = self.fan_speed {
device.fan_speed = v.min(5);
@@ -283,3 +447,20 @@ impl From<&Device> for ManualDeviceBaseline {
}
}
}
#[cfg(test)]
mod connection_type_tests {
use super::*;
#[test]
fn legacy_device_without_connection_type_defaults_to_local() {
let device = Device::simulated_default();
let mut value = serde_json::to_value(device).expect("serialize device");
value
.as_object_mut()
.expect("device object")
.remove("connection_type");
let migrated: Device = serde_json::from_value(value).expect("deserialize legacy device");
assert_eq!(migrated.connection_type, ConnectionType::Local);
}
}
+21
View File
@@ -49,3 +49,24 @@ pub struct EventLog {
pub message: String,
pub metadata: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnergyReading {
pub id: i64,
pub device_id: String,
pub timestamp: DateTime<Utc>,
pub source: String,
/// Original numeric value as reported by the source before unit normalization.
pub raw_meter_value: f64,
pub raw_unit: String,
/// Cumulative meter normalized to kWh.
pub normalized_meter_kwh: f64,
/// Consumption since the previous valid sample. Never negative.
pub consumption_kwh: f64,
#[serde(default)]
pub current_power_kw: Option<f64>,
pub quality: String,
#[serde(default)]
pub reset_detected: bool,
}
+4
View File
@@ -194,6 +194,10 @@ pub struct DebugSettings {
pub overlay_enabled: bool,
#[serde(default)]
pub gree_frames: bool,
#[serde(default)]
pub cloud_requests: bool,
#[serde(default)]
pub cloud_mqtt: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
+43
View File
@@ -1,3 +1,44 @@
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GreeCloudSettings {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_cloud_region")]
pub region: String,
#[serde(default)]
pub username: String,
#[serde(default)]
pub password: String,
#[serde(default = "default_cloud_poll_interval_seconds")]
pub polling_interval_seconds: u64,
/// Random, non-personal installation identifier used in the HTTP User-Agent.
#[serde(default)]
pub installation_id: String,
/// Reserved account key so the model can grow to multiple GREE accounts without changing devices.
#[serde(default)]
pub account_id: String,
#[serde(default)]
pub last_successful_contact: Option<DateTime<Utc>>,
/// Duration of the most recent successful REST operation against GREE Cloud.
#[serde(default)]
pub last_rest_response_time_ms: Option<u64>,
}
impl Default for GreeCloudSettings {
fn default() -> Self {
Self {
enabled: false,
region: default_cloud_region(),
username: String::new(),
password: String::new(),
polling_interval_seconds: default_cloud_poll_interval_seconds(),
installation_id: String::new(),
account_id: "default".into(),
last_successful_contact: None,
last_rest_response_time_ms: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeSettings {
pub controller_id: String,
@@ -39,6 +80,8 @@ pub struct RuntimeSettings {
#[serde(default)]
pub night_mode: NightModeSettings,
#[serde(default)]
pub gree_cloud: GreeCloudSettings,
#[serde(default)]
pub notifications: NotificationSettings,
pub home_assistant: HomeAssistantSettings,
}
+24
View File
@@ -15,6 +15,29 @@ pub struct GreeSettings {
pub compressor_protection_seconds: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GreeCloudSettingsUpdate {
pub enabled: bool,
pub region: String,
pub username: String,
#[serde(default)]
pub password: Option<String>,
pub polling_interval_seconds: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GreeCloudSettingsView {
pub enabled: bool,
pub region: String,
pub username: String,
pub password_configured: bool,
pub polling_interval_seconds: u64,
pub installation_id: String,
pub account_id: String,
pub last_successful_contact: Option<DateTime<Utc>>,
pub last_rest_response_time_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistorySettings {
pub retention_days: u32,
@@ -116,6 +139,7 @@ pub struct HomeAssistantSettingsView {
pub struct SettingsSnapshot {
pub application: ApplicationSettings,
pub gree: GreeSettings,
pub gree_cloud: GreeCloudSettingsView,
pub history: HistorySettings,
pub influxdb: InfluxDbSettingsView,
pub notifications: NotificationSettingsView,