use super::ffi;
use huesmith_core::hue::scene::{
fields_from_scene_state, parse_add_scene_payload, read_u16_le, scene_state_from_fields,
SceneField, MAX_SCENE_FIELDS,
};
use huesmith_core::light::state::{ColorMode, LightState, SceneState};
const MAX_SCENE_RESPONSE_ASDU: usize = 16;
const ZCL_FRAME_CONTROL_CLUSTER_RESPONSE: u8 = 0x19;
static mut SCENE_RESPONSE_ASDU: [u8; MAX_SCENE_RESPONSE_ASDU] = [0; MAX_SCENE_RESPONSE_ASDU];
pub fn register_privilege_commands(endpoint: u8) {
for command in [
ffi::CMD_SCENES_ADD_SCENE,
ffi::CMD_SCENES_ENHANCED_ADD_SCENE,
] {
let ret = unsafe {
ffi::esp_zb_zcl_add_privilege_command(endpoint, ffi::CLUSTER_SCENES, command as u16)
};
log::info!(
"Scenes privilege command: ep={} cluster=0x{:04X} cmd=0x{:02X} ret={}",
endpoint,
ffi::CLUSTER_SCENES,
command,
ret,
);
}
}
pub unsafe fn handle_privilege_command(message: *const core::ffi::c_void) -> i32 {
if message.is_null() {
log::warn!("Scenes privilege command: NULL message");
return 0;
}
let msg = &*(message as *const ffi::esp_zb_zcl_privilege_command_message_t);
if msg.info.cluster != ffi::CLUSTER_SCENES {
return 0;
}
let cmd_id = msg.info.command.id;
if cmd_id != ffi::CMD_SCENES_ADD_SCENE && cmd_id != ffi::CMD_SCENES_ENHANCED_ADD_SCENE {
log::debug!("Unhandled privileged scene command: 0x{:02X}", cmd_id);
return 0;
}
let payload = if msg.data.is_null() || msg.size == 0 {
&[]
} else {
core::slice::from_raw_parts(msg.data as *const u8, msg.size as usize)
};
if !payload.is_empty() {
log::debug!(
"Add Scene raw payload (len={}): {:02X?}",
payload.len(),
&payload[..payload.len().min(32)]
);
}
let fallback_group = read_u16_le(payload, 0).unwrap_or(0);
let fallback_scene = payload.get(2).copied().unwrap_or(0);
let status = match parse_add_scene_payload(payload, cmd_id) {
Ok(parsed) => {
log::info!(
"Add Scene: group=0x{:04X} scene={} transition={} fields={} cmd=0x{:02X}",
parsed.group_id,
parsed.scene_id,
parsed.transition_time,
parsed.fields.len(),
cmd_id,
);
let ret = store_scene_fields(
msg.info.dst_endpoint,
parsed.group_id,
parsed.scene_id,
parsed.transition_time,
&parsed.fields,
);
if ret == 0 {
ffi::ZCL_STATUS_SUCCESS
} else {
log::warn!(
"Add Scene store failed: group=0x{:04X} scene={} ret={}",
parsed.group_id,
parsed.scene_id,
ret,
);
ffi::ZCL_STATUS_INVALID_FIELD
}
}
Err(err) => {
log::warn!(
"Add Scene rejected: cmd=0x{:02X} size={} ({err})",
cmd_id,
payload.len(),
);
err.zcl_status()
}
};
let response_cmd = if cmd_id == ffi::CMD_SCENES_ENHANCED_ADD_SCENE {
ffi::CMD_SCENES_ENHANCED_ADD_SCENE
} else {
ffi::CMD_SCENES_ADD_SCENE
};
send_operate_scene_response(
&msg.info,
response_cmd,
status,
fallback_group,
fallback_scene,
);
0
}
pub unsafe fn handle_store_scene(
message: *const core::ffi::c_void,
light_state: Option<&core::cell::RefCell<LightState>>,
has_color_control: bool,
) -> i32 {
if message.is_null() {
log::warn!("Store Scene: NULL message");
return -1;
}
let msg = &*(message as *const ffi::esp_zb_zcl_store_scene_message_t);
if msg.info.status != ffi::ZCL_STATUS_SUCCESS as u32 {
log::warn!(
"Store Scene failed before app handler: status={}",
msg.info.status
);
return -1;
}
let Some(light_state) = light_state else {
log::warn!("Store Scene: light state unavailable");
return -1;
};
let snapshot = light_state.borrow().scene_snapshot();
let fields = fields_from_scene_state(&snapshot, has_color_control);
let ret = store_scene_fields(
msg.info.dst_endpoint,
msg.group_id,
msg.scene_id,
0,
&fields,
);
log::info!(
"Store Scene: group=0x{:04X} scene={} fields={} ret={}",
msg.group_id,
msg.scene_id,
fields.len(),
ret,
);
ret
}
pub unsafe fn handle_recall_scene(
message: *const core::ffi::c_void,
light_state: Option<&core::cell::RefCell<LightState>>,
has_color_control: bool,
) -> i32 {
if message.is_null() {
log::warn!("Recall Scene: NULL message");
return 0;
}
let msg = &*(message as *const ffi::esp_zb_zcl_recall_scene_message_t);
if msg.info.status != ffi::ZCL_STATUS_SUCCESS as u32 {
log::warn!(
"Recall Scene failed before app handler: status={}",
msg.info.status
);
return 0;
}
let fields = fields_from_extension_list(msg.field_set);
let scene = scene_state_from_fields(&fields);
log::info!(
"Recall Scene: group=0x{:04X} scene={} transition={} fields={}",
msg.group_id,
msg.scene_id,
msg.transition_time,
fields.len(),
);
if let Some(light_state) = light_state {
light_state
.borrow_mut()
.apply_scene_state_with_transition(&scene, msg.transition_time);
sync_zcl_attributes(msg.info.dst_endpoint, &scene, has_color_control);
} else {
log::warn!("Recall Scene: light state unavailable");
}
0
}
struct SceneExtensionFieldList {
_owned_data: Vec<Vec<u8>>,
nodes: Vec<ffi::esp_zb_zcl_scenes_extension_field_t>,
}
impl SceneExtensionFieldList {
fn from_fields(fields: &[SceneField]) -> Self {
let mut owned_data: Vec<Vec<u8>> = Vec::with_capacity(fields.len());
owned_data.extend(fields.iter().map(|f| f.data.clone()));
let mut nodes: Vec<ffi::esp_zb_zcl_scenes_extension_field_t> =
Vec::with_capacity(fields.len());
nodes.extend(fields.iter().enumerate().map(|(i, field)| {
ffi::esp_zb_zcl_scenes_extension_field_t {
cluster_id: field.cluster_id,
length: owned_data[i].len().min(u8::MAX as usize) as u8,
extension_field_attribute_value_list: owned_data[i].as_mut_ptr(),
next: core::ptr::null_mut(),
}
}));
for i in 0..nodes.len().saturating_sub(1) {
nodes[i].next = unsafe { nodes.as_mut_ptr().add(i + 1) };
}
Self {
_owned_data: owned_data,
nodes,
}
}
fn head(&mut self) -> *mut ffi::esp_zb_zcl_scenes_extension_field_t {
self.nodes.first_mut().map_or(core::ptr::null_mut(), |n| {
n as *mut ffi::esp_zb_zcl_scenes_extension_field_t
})
}
}
fn store_scene_fields(
endpoint: u8,
group_id: u16,
scene_id: u8,
transition_time: u16,
fields: &[SceneField],
) -> i32 {
let mut field_list = SceneExtensionFieldList::from_fields(fields);
let head = field_list.head();
let ret = unsafe {
ffi::esp_zb_zcl_scenes_table_store(endpoint, group_id, scene_id, transition_time, head)
};
if ret == 0 {
unsafe { ffi::esp_zb_zcl_scenes_table_show(endpoint) };
}
ret
}
fn fields_from_extension_list(
mut field: *mut ffi::esp_zb_zcl_scenes_extension_field_t,
) -> Vec<SceneField> {
let mut out = Vec::new();
let mut guard = 0usize;
while !field.is_null() && guard < MAX_SCENE_FIELDS {
let item = unsafe { &*field };
let data = if item.extension_field_attribute_value_list.is_null() || item.length == 0 {
Vec::new()
} else {
unsafe {
core::slice::from_raw_parts(
item.extension_field_attribute_value_list,
item.length as usize,
)
}
.to_vec()
};
out.push(SceneField {
cluster_id: item.cluster_id,
data,
});
field = item.next;
guard += 1;
}
out
}
fn sync_zcl_attributes(endpoint: u8, scene: &SceneState, has_color_control: bool) {
if let Some(on) = scene.on {
set_attr(
endpoint,
ffi::CLUSTER_ON_OFF,
ffi::ATTR_ON_OFF,
&on as *const bool as *const core::ffi::c_void,
);
}
if let Some(level) = scene.brightness {
set_attr(
endpoint,
ffi::CLUSTER_LEVEL_CONTROL,
ffi::ATTR_LEVEL_CURRENT_LEVEL,
&level as *const u8 as *const core::ffi::c_void,
);
}
if !has_color_control {
return;
}
sync_color_zcl_attributes(endpoint, scene);
}
fn sync_color_zcl_attributes(endpoint: u8, scene: &SceneState) {
if let Some(x) = scene.color_x {
set_attr(
endpoint,
ffi::CLUSTER_COLOR_CONTROL,
ffi::ATTR_COLOR_CURRENT_X,
&x as *const u16 as *const core::ffi::c_void,
);
}
if let Some(y) = scene.color_y {
set_attr(
endpoint,
ffi::CLUSTER_COLOR_CONTROL,
ffi::ATTR_COLOR_CURRENT_Y,
&y as *const u16 as *const core::ffi::c_void,
);
}
if let Some(temp) = scene.color_temp_mireds {
set_attr(
endpoint,
ffi::CLUSTER_COLOR_CONTROL,
ffi::ATTR_COLOR_TEMPERATURE_MIREDS,
&temp as *const u16 as *const core::ffi::c_void,
);
}
if let Some(enhanced_hue) = scene.enhanced_hue {
let hue = huesmith_core::color::enhanced_hue_to_hue(enhanced_hue);
set_attr(
endpoint,
ffi::CLUSTER_COLOR_CONTROL,
ffi::ATTR_COLOR_CURRENT_HUE,
&hue as *const u8 as *const core::ffi::c_void,
);
set_attr(
endpoint,
ffi::CLUSTER_COLOR_CONTROL,
ffi::ATTR_COLOR_ENHANCED_CURRENT_HUE,
&enhanced_hue as *const u16 as *const core::ffi::c_void,
);
} else if let Some(hue) = scene.hue {
set_attr(
endpoint,
ffi::CLUSTER_COLOR_CONTROL,
ffi::ATTR_COLOR_CURRENT_HUE,
&hue as *const u8 as *const core::ffi::c_void,
);
}
if let Some(saturation) = scene.saturation {
set_attr(
endpoint,
ffi::CLUSTER_COLOR_CONTROL,
ffi::ATTR_COLOR_CURRENT_SATURATION,
&saturation as *const u8 as *const core::ffi::c_void,
);
}
if let Some(mode) = scene.color_mode {
let color_mode = match mode {
ColorMode::HueSaturation => ffi::COLOR_MODE_HUE_SAT,
ColorMode::XY => ffi::COLOR_MODE_XY,
ColorMode::ColorTemperature => ffi::COLOR_MODE_COLOR_TEMP,
};
let enhanced_mode = match mode {
ColorMode::HueSaturation if scene.enhanced_hue.is_some() => {
ffi::ENHANCED_COLOR_MODE_ENHANCED_HUE_SAT
}
ColorMode::HueSaturation => ffi::ENHANCED_COLOR_MODE_HUE_SAT,
ColorMode::XY => ffi::ENHANCED_COLOR_MODE_XY,
ColorMode::ColorTemperature => ffi::ENHANCED_COLOR_MODE_COLOR_TEMP,
};
set_attr(
endpoint,
ffi::CLUSTER_COLOR_CONTROL,
ffi::ATTR_COLOR_MODE,
&color_mode as *const u8 as *const core::ffi::c_void,
);
set_attr(
endpoint,
ffi::CLUSTER_COLOR_CONTROL,
ffi::ATTR_COLOR_ENHANCED_COLOR_MODE,
&enhanced_mode as *const u8 as *const core::ffi::c_void,
);
}
}
fn set_attr(endpoint: u8, cluster: u16, attr: u16, value: *const core::ffi::c_void) {
let ret = unsafe {
ffi::esp_zb_zcl_set_attribute_val(
endpoint,
cluster,
ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
attr,
value,
false,
)
};
if ret != 0 {
log::debug!(
"Scene attribute sync failed: ep={} cluster=0x{:04X} attr=0x{:04X} ret={}",
endpoint,
cluster,
attr,
ret,
);
}
}
fn send_operate_scene_response(
info: &ffi::esp_zb_zcl_cmd_info_t,
command_id: u8,
status: u8,
group_id: u16,
scene_id: u8,
) {
let mut payload = Vec::with_capacity(4);
payload.push(status);
payload.extend_from_slice(&group_id.to_le_bytes());
payload.push(scene_id);
send_scene_response(info, command_id, &payload);
}
fn send_scene_response(info: &ffi::esp_zb_zcl_cmd_info_t, command_id: u8, payload: &[u8]) {
let Some(dst_short) = info.src_address.short_addr() else {
log::warn!(
"Scene response skipped: unsupported source address type {}",
info.src_address.addr_type,
);
return;
};
let asdu_length = 3usize + payload.len();
if asdu_length > MAX_SCENE_RESPONSE_ASDU {
log::warn!("Scene response skipped: ASDU too large ({})", asdu_length);
return;
}
let asdu = core::ptr::addr_of_mut!(SCENE_RESPONSE_ASDU) as *mut u8;
unsafe {
asdu.add(0).write(ZCL_FRAME_CONTROL_CLUSTER_RESPONSE);
asdu.add(1).write(info.header.tsn);
asdu.add(2).write(command_id);
core::ptr::copy_nonoverlapping(payload.as_ptr(), asdu.add(3), payload.len());
}
let mut req = ffi::esp_zb_apsde_data_req_t {
dst_addr_mode: ffi::ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT,
dst_addr: ffi::esp_zb_addr_u {
addr_short: dst_short,
},
dst_endpoint: info.src_endpoint,
profile_id: info.profile,
cluster_id: ffi::CLUSTER_SCENES,
src_endpoint: info.dst_endpoint,
asdu_length: asdu_length as u32,
asdu,
tx_options: ffi::ESP_ZB_APSDE_TX_OPT_SECURITY_ENABLED | ffi::ESP_ZB_APSDE_TX_OPT_ACK_TX,
use_alias: false,
alias_src_addr: 0,
alias_seq_num: 0,
radius: 0,
};
let ret = unsafe { ffi::esp_zb_aps_data_request(&mut req) };
log::debug!(
"Scene response: dst=0x{:04X} src_ep={} dst_ep={} cmd=0x{:02X} status_ret={}",
dst_short,
info.dst_endpoint,
info.src_endpoint,
command_id,
ret,
);
}