v0.14.0
This commit is contained in:
1 parent
c3fbc5ccc6
commit
021cbddba5
68 files changed
+7780
-202
No files matched your search
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user