Skip to content

Commit

Permalink
merge in SKU scene data when listing scenes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
wez committed Jan 4, 2024
1 parent 69ed27f commit 9e3aeef
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 36 deletions.
69 changes: 41 additions & 28 deletions src/platform_api.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -207,13 +209,24 @@ impl GoveeApiClient {
.await
}

pub async fn list_scene_names(&self, device: &HttpDeviceInfo) -> anyhow::Result<Vec<String>> {
pub async fn get_scene_caps(
&self,
device: &HttpDeviceInfo,
) -> anyhow::Result<Vec<DeviceCapability>> {
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,
Expand All @@ -222,47 +235,47 @@ 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<Vec<String>> {
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(
&self,
device: &HttpDeviceInfo,
scene: &str,
) -> anyhow::Result<ControlDeviceResponseCapability> {
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");
Expand Down
13 changes: 7 additions & 6 deletions src/service/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,9 @@ impl State {
pub async fn device_list_scenes(&self, device: &Device) -> anyhow::Result<Vec<String>> {
// TODO: some plumbing to maintain offline scene controls for preferred-LAN control

fn sort_scenes(mut scenes: Vec<String>) -> Vec<String> {
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?));
}
}

Expand Down Expand Up @@ -272,3 +267,9 @@ impl State {
Ok(())
}
}

pub fn sort_and_dedup_scenes(mut scenes: Vec<String>) -> Vec<String> {
scenes.sort_by_key(|s| s.to_ascii_lowercase());
scenes.dedup();
scenes
}
38 changes: 36 additions & 2 deletions src/undoc_api.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Vec<DeviceCapability>> {
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,
Expand Down

0 comments on commit 9e3aeef

Please sign in to comment.