use async_trait::async_trait;
use crate::homeauto::matter::clusters::tlv;
use crate::homeauto::matter::data_model::ClusterServer;
use crate::homeauto::matter::error::{MatterError, MatterResult};
use crate::homeauto::matter::types::MatterDeviceConfig;
pub const ATTR_DATA_MODEL_REVISION: u32 = 0x0000;
pub const ATTR_VENDOR_NAME: u32 = 0x0001;
pub const ATTR_VENDOR_ID: u32 = 0x0002;
pub const ATTR_PRODUCT_NAME: u32 = 0x0003;
pub const ATTR_PRODUCT_ID: u32 = 0x0004;
pub const ATTR_NODE_LABEL: u32 = 0x0005;
pub const ATTR_LOCATION: u32 = 0x0006;
pub const ATTR_HARDWARE_VERSION: u32 = 0x0007;
pub const ATTR_SOFTWARE_VERSION: u32 = 0x000A;
pub const ATTR_SOFTWARE_VERSION_STRING: u32 = 0x000B;
pub const ATTR_CAPABILITY_MINIMA: u32 = 0x000F;
const CLUSTER_ID: u32 = 0x0028;
fn tlv_uint16(tag: u8, val: u16) -> Vec<u8> {
let mut v = vec![tlv::TAG_CONTEXT_1 | tlv::TYPE_UNSIGNED_INT_2, tag];
v.extend_from_slice(&val.to_le_bytes());
v
}
fn tlv_uint32(tag: u8, val: u32) -> Vec<u8> {
let mut v = vec![tlv::TAG_CONTEXT_1 | tlv::TYPE_UNSIGNED_INT_4, tag];
v.extend_from_slice(&val.to_le_bytes());
v
}
fn tlv_utf8_string(tag: u8, s: &str) -> Vec<u8> {
let bytes = s.as_bytes();
let mut v = vec![tlv::TAG_CONTEXT_1 | 0x0C, tag, bytes.len() as u8];
v.extend_from_slice(bytes);
v
}
fn wrap_struct(inner: &[u8]) -> Vec<u8> {
let mut v = vec![tlv::TYPE_STRUCTURE];
v.extend_from_slice(inner);
v.push(tlv::TYPE_END_OF_CONTAINER);
v
}
pub struct BasicInformationCluster {
vendor_name: String,
vendor_id: u16,
product_name: String,
product_id: u16,
node_label: String,
}
impl BasicInformationCluster {
pub fn new(config: &MatterDeviceConfig) -> Self {
Self {
vendor_name: "Brainwires".to_string(),
vendor_id: config.vendor_id,
product_name: config.device_name.clone(),
product_id: config.product_id,
node_label: config.device_name.clone(),
}
}
}
#[async_trait]
impl ClusterServer for BasicInformationCluster {
fn cluster_id(&self) -> u32 {
CLUSTER_ID
}
async fn read_attribute(&self, attr_id: u32) -> MatterResult<Vec<u8>> {
match attr_id {
ATTR_DATA_MODEL_REVISION => {
Ok(tlv_uint16(0, 1))
}
ATTR_VENDOR_NAME => Ok(tlv_utf8_string(0, &self.vendor_name)),
ATTR_VENDOR_ID => Ok(tlv_uint16(0, self.vendor_id)),
ATTR_PRODUCT_NAME => Ok(tlv_utf8_string(0, &self.product_name)),
ATTR_PRODUCT_ID => Ok(tlv_uint16(0, self.product_id)),
ATTR_NODE_LABEL => Ok(tlv_utf8_string(0, &self.node_label)),
ATTR_LOCATION => Ok(tlv_utf8_string(0, "XX")),
ATTR_HARDWARE_VERSION => Ok(tlv_uint16(0, 0)),
ATTR_SOFTWARE_VERSION => Ok(tlv_uint32(0, 1)),
ATTR_SOFTWARE_VERSION_STRING => Ok(tlv_utf8_string(0, "1.0.0")),
ATTR_CAPABILITY_MINIMA => {
let mut inner = tlv_uint16(0, 3);
inner.extend_from_slice(&tlv_uint16(1, 3));
Ok(wrap_struct(&inner))
}
_ => Err(MatterError::Transport("unsupported attribute".into())),
}
}
async fn write_attribute(&self, _attr_id: u32, _value: &[u8]) -> MatterResult<()> {
Err(MatterError::Transport(
"BasicInformation attributes are read-only".into(),
))
}
async fn invoke_command(&self, _cmd_id: u32, _args: &[u8]) -> MatterResult<Vec<u8>> {
Err(MatterError::Transport(
"BasicInformation has no commands".into(),
))
}
fn attribute_ids(&self) -> Vec<u32> {
vec![
ATTR_DATA_MODEL_REVISION,
ATTR_VENDOR_NAME,
ATTR_VENDOR_ID,
ATTR_PRODUCT_NAME,
ATTR_PRODUCT_ID,
ATTR_NODE_LABEL,
ATTR_LOCATION,
ATTR_HARDWARE_VERSION,
ATTR_SOFTWARE_VERSION,
ATTR_SOFTWARE_VERSION_STRING,
ATTR_CAPABILITY_MINIMA,
]
}
fn command_ids(&self) -> Vec<u32> {
vec![]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::homeauto::matter::types::MatterDeviceConfig;
fn make_cluster() -> BasicInformationCluster {
let config = MatterDeviceConfig::builder()
.device_name("Test Light")
.vendor_id(0xFFF1)
.product_id(0x8001)
.build();
BasicInformationCluster::new(&config)
}
#[tokio::test]
async fn basic_info_vendor_id_attribute_returns_correct_tlv() {
let cluster = make_cluster();
let data = cluster
.read_attribute(ATTR_VENDOR_ID)
.await
.expect("VendorID read failed");
assert_eq!(data.len(), 4, "VendorID TLV should be 4 bytes");
let value = u16::from_le_bytes([data[2], data[3]]);
assert_eq!(value, 0xFFF1);
}
#[tokio::test]
async fn basic_info_unknown_attribute_returns_error() {
let cluster = make_cluster();
let result = cluster.read_attribute(0xFFFF).await;
assert!(result.is_err(), "Unknown attribute should return an error");
}
}