use super::types::cluster_id;
#[allow(dead_code)]
pub(super) mod tlv {
pub const TYPE_SIGNED_INT_1: u8 = 0x00;
pub const TYPE_SIGNED_INT_2: u8 = 0x01;
pub const TYPE_SIGNED_INT_4: u8 = 0x02;
pub const TYPE_UNSIGNED_INT_1: u8 = 0x04;
pub const TYPE_UNSIGNED_INT_2: u8 = 0x05;
pub const TYPE_UNSIGNED_INT_4: u8 = 0x06;
pub const TYPE_BOOL_FALSE: u8 = 0x08;
pub const TYPE_BOOL_TRUE: u8 = 0x09;
pub const TYPE_NULL: u8 = 0x14;
pub const TYPE_STRUCTURE: u8 = 0x15;
pub const TYPE_ARRAY: u8 = 0x16;
pub const TYPE_LIST: u8 = 0x17;
pub const TYPE_END_OF_CONTAINER: u8 = 0x18;
pub const TAG_ANONYMOUS: u8 = 0x00; pub const TAG_CONTEXT_1: u8 = 0x20; }
pub(super) fn tlv_uint8(tag: u8, val: u8) -> Vec<u8> {
vec![tlv::TAG_CONTEXT_1 | tlv::TYPE_UNSIGNED_INT_1, tag, val]
}
pub(super) 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
}
pub(super) 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
}
pub(super) fn tlv_bool(tag: u8, val: bool) -> Vec<u8> {
let ty = if val {
tlv::TYPE_BOOL_TRUE
} else {
tlv::TYPE_BOOL_FALSE
};
vec![tlv::TAG_CONTEXT_1 | ty, tag]
}
pub(super) fn tlv_null(tag: u8) -> Vec<u8> {
vec![tlv::TAG_CONTEXT_1 | tlv::TYPE_NULL, tag]
}
pub(super) 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(super) fn wrap_struct_tagged(tag: u8, inner: &[u8]) -> Vec<u8> {
let mut v = vec![tlv::TAG_CONTEXT_1 | tlv::TYPE_STRUCTURE, tag];
v.extend_from_slice(inner);
v.push(tlv::TYPE_END_OF_CONTAINER);
v
}
pub(super) fn wrap_list_tagged(tag: u8, inner: &[u8]) -> Vec<u8> {
let mut v = vec![tlv::TAG_CONTEXT_1 | tlv::TYPE_LIST, tag];
v.extend_from_slice(inner);
v.push(tlv::TYPE_END_OF_CONTAINER);
v
}
pub(super) fn read_u16_le(bytes: &[u8], offset: usize) -> Option<(u16, usize)> {
if offset + 2 > bytes.len() {
return None;
}
let v = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
Some((v, offset + 2))
}
pub(super) fn read_u32_le(bytes: &[u8], offset: usize) -> Option<(u32, usize)> {
if offset + 4 > bytes.len() {
return None;
}
let v = u32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
]);
Some((v, offset + 4))
}
#[derive(Debug, Clone, PartialEq)]
pub struct AttributePath {
pub endpoint_id: Option<u16>,
pub cluster_id: Option<u32>,
pub attribute_id: Option<u32>,
}
impl AttributePath {
pub fn specific(endpoint_id: u16, cluster_id: u32, attribute_id: u32) -> Self {
Self {
endpoint_id: Some(endpoint_id),
cluster_id: Some(cluster_id),
attribute_id: Some(attribute_id),
}
}
pub fn wildcard() -> Self {
Self {
endpoint_id: None,
cluster_id: None,
attribute_id: None,
}
}
pub fn encode(&self) -> Vec<u8> {
let mut inner = Vec::new();
if let Some(ep) = self.endpoint_id {
inner.extend_from_slice(&tlv_uint16(2, ep));
}
if let Some(cl) = self.cluster_id {
inner.extend_from_slice(&tlv_uint32(3, cl));
}
if let Some(attr) = self.attribute_id {
inner.extend_from_slice(&tlv_uint32(4, attr));
}
wrap_struct(&inner)
}
pub fn decode(bytes: &[u8]) -> Option<Self> {
if bytes.is_empty() || bytes[0] != tlv::TYPE_STRUCTURE {
return None;
}
let mut endpoint_id = None;
let mut cluster_id = None;
let mut attribute_id = None;
let mut i = 1;
while i < bytes.len() {
if bytes[i] == tlv::TYPE_END_OF_CONTAINER {
break;
}
if i + 1 >= bytes.len() {
return None;
}
let ctrl = bytes[i];
let tag = bytes[i + 1];
i += 2;
let type_bits = ctrl & 0x1F; match (tag, type_bits) {
(2, t) if t == tlv::TYPE_UNSIGNED_INT_2 => {
let (v, next) = read_u16_le(bytes, i)?;
endpoint_id = Some(v);
i = next;
}
(3, t) if t == tlv::TYPE_UNSIGNED_INT_4 => {
let (v, next) = read_u32_le(bytes, i)?;
cluster_id = Some(v);
i = next;
}
(4, t) if t == tlv::TYPE_UNSIGNED_INT_4 => {
let (v, next) = read_u32_le(bytes, i)?;
attribute_id = Some(v);
i = next;
}
_ => return None, }
}
Some(Self {
endpoint_id,
cluster_id,
attribute_id,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CommandPath {
pub endpoint_id: u16,
pub cluster_id: u32,
pub command_id: u32,
}
impl CommandPath {
pub fn new(endpoint_id: u16, cluster_id: u32, command_id: u32) -> Self {
Self {
endpoint_id,
cluster_id,
command_id,
}
}
pub fn encode(&self) -> Vec<u8> {
let mut inner = Vec::new();
inner.extend_from_slice(&tlv_uint16(0, self.endpoint_id));
inner.extend_from_slice(&tlv_uint32(1, self.cluster_id));
inner.extend_from_slice(&tlv_uint32(2, self.command_id));
wrap_struct(&inner)
}
pub fn decode(bytes: &[u8]) -> Option<Self> {
if bytes.is_empty() || bytes[0] != tlv::TYPE_STRUCTURE {
return None;
}
let mut endpoint_id = None::<u16>;
let mut cluster_id = None::<u32>;
let mut command_id = None::<u32>;
let mut i = 1;
while i < bytes.len() {
if bytes[i] == tlv::TYPE_END_OF_CONTAINER {
break;
}
if i + 1 >= bytes.len() {
return None;
}
let ctrl = bytes[i];
let tag = bytes[i + 1];
i += 2;
let type_bits = ctrl & 0x1F;
match (tag, type_bits) {
(0, t) if t == tlv::TYPE_UNSIGNED_INT_2 => {
let (v, next) = read_u16_le(bytes, i)?;
endpoint_id = Some(v);
i = next;
}
(1, t) if t == tlv::TYPE_UNSIGNED_INT_4 => {
let (v, next) = read_u32_le(bytes, i)?;
cluster_id = Some(v);
i = next;
}
(2, t) if t == tlv::TYPE_UNSIGNED_INT_4 => {
let (v, next) = read_u32_le(bytes, i)?;
command_id = Some(v);
i = next;
}
_ => return None,
}
}
Some(Self {
endpoint_id: endpoint_id?,
cluster_id: cluster_id?,
command_id: command_id?,
})
}
}
pub mod on_off {
use super::*;
pub const CLUSTER_ID: u32 = cluster_id::ON_OFF;
pub const ATTR_ON_OFF: u32 = 0x0000;
pub const CMD_OFF: u32 = 0x00;
pub const CMD_ON: u32 = 0x01;
pub const CMD_TOGGLE: u32 = 0x02;
pub fn on_tlv() -> Vec<u8> {
wrap_struct(&[])
}
pub fn off_tlv() -> Vec<u8> {
wrap_struct(&[])
}
pub fn toggle_tlv() -> Vec<u8> {
wrap_struct(&[])
}
}
pub mod level_control {
use super::*;
pub const CLUSTER_ID: u32 = cluster_id::LEVEL_CONTROL;
pub const ATTR_CURRENT_LEVEL: u32 = 0x0000;
pub const ATTR_REMAINING_TIME: u32 = 0x0001;
pub const ATTR_ON_LEVEL: u32 = 0x0011;
pub const CMD_MOVE_TO_LEVEL: u32 = 0x00;
pub const CMD_MOVE: u32 = 0x01;
pub const CMD_STEP: u32 = 0x02;
pub const CMD_STOP: u32 = 0x03;
pub const CMD_MOVE_TO_LEVEL_WITH_ON_OFF: u32 = 0x04;
pub fn move_to_level_tlv(level: u8, transition_time_tenths: Option<u16>) -> Vec<u8> {
let mut inner = tlv_uint8(0, level);
inner.extend_from_slice(&match transition_time_tenths {
Some(t) => tlv_uint16(1, t),
None => tlv_null(1),
});
inner.extend_from_slice(&tlv_uint8(2, 0)); inner.extend_from_slice(&tlv_uint8(3, 0)); wrap_struct(&inner)
}
}
pub mod color_control {
use super::*;
pub const CLUSTER_ID: u32 = cluster_id::COLOR_CONTROL;
pub const ATTR_CURRENT_HUE: u32 = 0x0000;
pub const ATTR_CURRENT_SAT: u32 = 0x0001;
pub const ATTR_COLOR_TEMP_MIREDS: u32 = 0x0007;
pub const ATTR_COLOR_MODE: u32 = 0x0008;
pub const CMD_MOVE_TO_HUE: u32 = 0x00;
pub const CMD_MOVE_TO_SAT: u32 = 0x03;
pub const CMD_MOVE_TO_HUE_AND_SAT: u32 = 0x06;
pub const CMD_MOVE_TO_COLOR_TEMP: u32 = 0x0A;
pub fn move_to_hue_and_sat_tlv(hue: u8, sat: u8, transition_time_tenths: u16) -> Vec<u8> {
let mut inner = tlv_uint8(0, hue);
inner.extend_from_slice(&tlv_uint8(1, sat));
inner.extend_from_slice(&tlv_uint16(2, transition_time_tenths));
inner.extend_from_slice(&tlv_uint8(3, 0)); inner.extend_from_slice(&tlv_uint8(4, 0)); wrap_struct(&inner)
}
pub fn move_to_color_temp_tlv(mireds: u16, transition_time_tenths: u16) -> Vec<u8> {
let mut inner = tlv_uint16(0, mireds);
inner.extend_from_slice(&tlv_uint16(1, transition_time_tenths));
inner.extend_from_slice(&tlv_uint8(2, 0));
inner.extend_from_slice(&tlv_uint8(3, 0));
wrap_struct(&inner)
}
}
pub mod thermostat {
use super::*;
pub const CLUSTER_ID: u32 = cluster_id::THERMOSTAT;
pub const ATTR_LOCAL_TEMP: u32 = 0x0000;
pub const ATTR_OCCUPIED_COOLING_SETPOINT: u32 = 0x0011;
pub const ATTR_OCCUPIED_HEATING_SETPOINT: u32 = 0x0012;
pub const ATTR_SYSTEM_MODE: u32 = 0x001C;
pub const CMD_SET_WEEKLY_SCHEDULE: u32 = 0x01;
pub const CMD_SET_SETPOINT_RAISE_LOWER: u32 = 0x00;
pub fn setpoint_raise_lower_tlv(mode: u8, amount: i8) -> Vec<u8> {
let mut inner = tlv_uint8(0, mode);
inner.push(tlv::TAG_CONTEXT_1 | tlv::TYPE_SIGNED_INT_1);
inner.push(1);
inner.push(amount as u8);
wrap_struct(&inner)
}
}
pub mod door_lock {
use super::*;
pub const CLUSTER_ID: u32 = cluster_id::DOOR_LOCK;
pub const ATTR_LOCK_STATE: u32 = 0x0000;
pub const ATTR_LOCK_TYPE: u32 = 0x0001;
pub const CMD_LOCK_DOOR: u32 = 0x00;
pub const CMD_UNLOCK_DOOR: u32 = 0x01;
pub fn lock_tlv(pin: Option<&[u8]>) -> Vec<u8> {
let inner = if let Some(p) = pin {
let mut v = vec![0x30u8, 0, p.len() as u8];
v.extend_from_slice(p);
v
} else {
vec![]
};
wrap_struct(&inner)
}
}
pub mod window_covering {
use super::*;
pub const CLUSTER_ID: u32 = cluster_id::WINDOW_COVERING;
pub const ATTR_CURRENT_POSITION_LIFT_PCT: u32 = 0x0008;
pub const ATTR_CURRENT_POSITION_TILT_PCT: u32 = 0x0009;
pub const CMD_UP_OR_OPEN: u32 = 0x00;
pub const CMD_DOWN_OR_CLOSE: u32 = 0x01;
pub const CMD_STOP_MOTION: u32 = 0x02;
pub const CMD_GO_TO_LIFT_PERCENTAGE: u32 = 0x05;
pub const CMD_GO_TO_TILT_PERCENTAGE: u32 = 0x08;
pub fn go_to_lift_percentage_tlv(percent: u8) -> Vec<u8> {
let inner = tlv_uint8(0, percent);
wrap_struct(&inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn on_off_command_encodes_correctly() {
let on = on_off::on_tlv();
assert_eq!(on, vec![tlv::TYPE_STRUCTURE, tlv::TYPE_END_OF_CONTAINER]);
let off = on_off::off_tlv();
assert_eq!(on, off); }
#[test]
fn level_control_move_to_level_tlv() {
let tlv = level_control::move_to_level_tlv(128, Some(10));
assert_eq!(tlv[0], 0x15); assert_eq!(*tlv.last().unwrap(), 0x18); assert!(tlv.contains(&128));
}
#[test]
fn thermostat_setpoint_tlv_roundtrip() {
let tlv = thermostat::setpoint_raise_lower_tlv(0, 10); assert_eq!(tlv[0], 0x15); assert_eq!(*tlv.last().unwrap(), 0x18);
}
#[test]
fn door_lock_lock_no_pin() {
let tlv = door_lock::lock_tlv(None);
assert_eq!(tlv, vec![0x15, 0x18]); }
#[test]
fn door_lock_lock_with_pin() {
let tlv = door_lock::lock_tlv(Some(b"1234"));
assert!(tlv.len() > 2);
assert_eq!(tlv[0], 0x15); }
#[test]
fn color_temp_tlv_encodes_mireds() {
let tlv = color_control::move_to_color_temp_tlv(300, 10);
assert_eq!(tlv[0], 0x15);
let has_mireds = tlv.windows(2).any(|w| w == [0x2C, 0x01]);
assert!(has_mireds);
}
#[test]
fn attribute_path_specific_encode_decode_roundtrip() {
let path = AttributePath::specific(1, 0x0006, 0x0000);
let encoded = path.encode();
assert_eq!(encoded[0], tlv::TYPE_STRUCTURE);
assert_eq!(*encoded.last().unwrap(), tlv::TYPE_END_OF_CONTAINER);
let decoded = AttributePath::decode(&encoded).expect("decode failed");
assert_eq!(decoded, path);
}
#[test]
fn attribute_path_wildcard_encode_has_no_fields() {
let path = AttributePath::wildcard();
let encoded = path.encode();
assert_eq!(
encoded,
vec![tlv::TYPE_STRUCTURE, tlv::TYPE_END_OF_CONTAINER]
);
let decoded = AttributePath::decode(&encoded).expect("decode failed");
assert_eq!(decoded, path);
}
#[test]
fn command_path_encode_decode_roundtrip() {
let path = CommandPath::new(0, 0x0006, 0x01);
let encoded = path.encode();
assert_eq!(encoded[0], tlv::TYPE_STRUCTURE);
assert_eq!(*encoded.last().unwrap(), tlv::TYPE_END_OF_CONTAINER);
let decoded = CommandPath::decode(&encoded).expect("decode failed");
assert_eq!(decoded, path);
}
}