From 9e3aeef8d4d982d271f05cba7fffc26e88de33b9 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Thu, 4 Jan 2024 07:47:12 -0700 Subject: [PATCH] merge in SKU scene data when listing scenes Today the Govee Platform API is only returning the first scene from the full list. Merge in what we can retrieve from the SKU light effect library so that we're not completely broken by this. --- src/platform_api.rs | 69 ++++++++++++++++++++++++++------------------ src/service/state.rs | 13 +++++---- src/undoc_api.rs | 38 ++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 36 deletions(-) diff --git a/src/platform_api.rs b/src/platform_api.rs index 5433f34..b6e21b4 100644 --- a/src/platform_api.rs +++ b/src/platform_api.rs @@ -1,5 +1,7 @@ use crate::cache::{cache_get, CacheComputeResult, CacheGetOptions}; use crate::opt_env_var; +use crate::service::state::sort_and_dedup_scenes; +use crate::undoc_api::GoveeUndocumentedApi; use anyhow::Context; use reqwest::Method; use serde::{Deserialize, Serialize}; @@ -207,13 +209,24 @@ impl GoveeApiClient { .await } - pub async fn list_scene_names(&self, device: &HttpDeviceInfo) -> anyhow::Result> { + pub async fn get_scene_caps( + &self, + device: &HttpDeviceInfo, + ) -> anyhow::Result> { let mut result = vec![]; let scene_caps = self.get_device_scenes(&device).await?; let diy_caps = self.get_device_diy_scenes(&device).await?; + let undoc_caps = + match GoveeUndocumentedApi::synthesize_platform_api_scene_list(&device.sku).await { + Ok(caps) => caps, + Err(err) => { + log::warn!("synthesize_platform_api_scene_list: {err:#}"); + vec![] + } + }; - for caps in [&device.capabilities, &scene_caps, &diy_caps] { + for caps in [&device.capabilities, &scene_caps, &diy_caps, &undoc_caps] { for cap in caps { let is_scene = matches!( cap.kind, @@ -222,18 +235,29 @@ impl GoveeApiClient { if !is_scene { continue; } - match &cap.parameters { - Some(DeviceParameters::Enum { options }) => { - for opt in options { - result.push(opt.name.to_string()); - } + result.push(cap.clone()); + } + } + + Ok(result) + } + + pub async fn list_scene_names(&self, device: &HttpDeviceInfo) -> anyhow::Result> { + let mut result = vec![]; + + let caps = self.get_scene_caps(device).await?; + for cap in caps { + match &cap.parameters { + Some(DeviceParameters::Enum { options }) => { + for opt in options { + result.push(opt.name.to_string()); } - _ => anyhow::bail!("unexpected type {cap:#?}"), } + _ => anyhow::bail!("unexpected type {cap:#?}"), } } - Ok(result) + Ok(sort_and_dedup_scenes(result)) } pub async fn set_scene_by_name( @@ -241,28 +265,17 @@ impl GoveeApiClient { device: &HttpDeviceInfo, scene: &str, ) -> anyhow::Result { - let scene_caps = self.get_device_scenes(&device).await?; - let diy_caps = self.get_device_diy_scenes(&device).await?; - - for caps in [&device.capabilities, &scene_caps, &diy_caps] { - for cap in caps { - let is_scene = matches!( - cap.kind, - DeviceCapabilityKind::DynamicScene | DeviceCapabilityKind::DynamicSetting - ); - if !is_scene { - continue; - } - match &cap.parameters { - Some(DeviceParameters::Enum { options }) => { - for opt in options { - if scene.eq_ignore_ascii_case(&opt.name) { - return self.control_device(&device, &cap, opt.value.clone()).await; - } + let caps = self.get_scene_caps(device).await?; + for cap in caps { + match &cap.parameters { + Some(DeviceParameters::Enum { options }) => { + for opt in options { + if scene.eq_ignore_ascii_case(&opt.name) { + return self.control_device(&device, &cap, opt.value.clone()).await; } } - _ => anyhow::bail!("unexpected type {cap:#?}"), } + _ => anyhow::bail!("unexpected type {cap:#?}"), } } anyhow::bail!("Scene '{scene}' is not available for this device"); diff --git a/src/service/state.rs b/src/service/state.rs index 38eaefd..3a012bc 100644 --- a/src/service/state.rs +++ b/src/service/state.rs @@ -231,14 +231,9 @@ impl State { pub async fn device_list_scenes(&self, device: &Device) -> anyhow::Result> { // TODO: some plumbing to maintain offline scene controls for preferred-LAN control - fn sort_scenes(mut scenes: Vec) -> Vec { - scenes.sort_by_key(|s| s.to_ascii_lowercase()); - scenes - } - if let Some(client) = self.get_platform_client().await { if let Some(info) = &device.http_device_info { - return Ok(sort_scenes(client.list_scene_names(info).await?)); + return Ok(sort_and_dedup_scenes(client.list_scene_names(info).await?)); } } @@ -272,3 +267,9 @@ impl State { Ok(()) } } + +pub fn sort_and_dedup_scenes(mut scenes: Vec) -> Vec { + scenes.sort_by_key(|s| s.to_ascii_lowercase()); + scenes.dedup(); + scenes +} diff --git a/src/undoc_api.rs b/src/undoc_api.rs index 4467963..9e77a84 100644 --- a/src/undoc_api.rs +++ b/src/undoc_api.rs @@ -1,11 +1,13 @@ use crate::cache::{cache_get, CacheComputeResult, CacheGetOptions}; use crate::lan_api::boolean_int; use crate::opt_env_var; -use crate::platform_api::http_response_body; +use crate::platform_api::{ + http_response_body, DeviceCapability, DeviceCapabilityKind, DeviceParameters, EnumOption, +}; use reqwest::Method; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; +use serde_json::{json, Value as JsonValue}; use std::path::PathBuf; use std::time::Duration; use uuid::Uuid; @@ -298,6 +300,38 @@ impl GoveeUndocumentedApi { .await } + /// This is present primarily to workaround a bug where Govee aren't returning + /// the full list of scenes via their supported platform API + pub async fn synthesize_platform_api_scene_list( + sku: &str, + ) -> anyhow::Result> { + let catalog = Self::get_scenes_for_device(sku).await?; + let mut options = vec![]; + + for c in catalog { + for s in c.scenes { + if let Some(param_id) = s.light_effects.get(0).map(|e| e.scence_param_id) { + options.push(EnumOption { + name: s.scene_name, + value: json!({ + "paramId": param_id, + "id": s.scene_id, + }), + extras: Default::default(), + }); + } + } + } + + Ok(vec![DeviceCapability { + kind: DeviceCapabilityKind::DynamicScene, + parameters: Some(DeviceParameters::Enum { options }), + alarm_type: None, + event_state: None, + instance: "lightScene".to_string(), + }]) + } + pub async fn get_saved_one_click_shortcuts( &self, community_token: &str,