#![allow(clippy::too_many_arguments)]
use crate::tlv;
use anyhow;
use serde_json;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum EndProductType {
Rollershade = 0,
Romanshade = 1,
Balloonshade = 2,
Wovenwood = 3,
Pleatedshade = 4,
Cellularshade = 5,
Layeredshade = 6,
Layeredshade2d = 7,
Sheershade = 8,
Tiltonlyinteriorblind = 9,
Interiorblind = 10,
Verticalblindstripcurtain = 11,
Interiorvenetianblind = 12,
Exteriorvenetianblind = 13,
Lateralleftcurtain = 14,
Lateralrightcurtain = 15,
Centralcurtain = 16,
Rollershutter = 17,
Exteriorverticalscreen = 18,
Awningterracepatio = 19,
Awningverticalscreen = 20,
Tiltonlypergola = 21,
Swingingshutter = 22,
Slidingshutter = 23,
Unknown = 255,
}
impl EndProductType {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(EndProductType::Rollershade),
1 => Some(EndProductType::Romanshade),
2 => Some(EndProductType::Balloonshade),
3 => Some(EndProductType::Wovenwood),
4 => Some(EndProductType::Pleatedshade),
5 => Some(EndProductType::Cellularshade),
6 => Some(EndProductType::Layeredshade),
7 => Some(EndProductType::Layeredshade2d),
8 => Some(EndProductType::Sheershade),
9 => Some(EndProductType::Tiltonlyinteriorblind),
10 => Some(EndProductType::Interiorblind),
11 => Some(EndProductType::Verticalblindstripcurtain),
12 => Some(EndProductType::Interiorvenetianblind),
13 => Some(EndProductType::Exteriorvenetianblind),
14 => Some(EndProductType::Lateralleftcurtain),
15 => Some(EndProductType::Lateralrightcurtain),
16 => Some(EndProductType::Centralcurtain),
17 => Some(EndProductType::Rollershutter),
18 => Some(EndProductType::Exteriorverticalscreen),
19 => Some(EndProductType::Awningterracepatio),
20 => Some(EndProductType::Awningverticalscreen),
21 => Some(EndProductType::Tiltonlypergola),
22 => Some(EndProductType::Swingingshutter),
23 => Some(EndProductType::Slidingshutter),
255 => Some(EndProductType::Unknown),
_ => None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
}
impl From<EndProductType> for u8 {
fn from(val: EndProductType) -> Self {
val as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum Type {
Rollershade = 0,
Rollershade2motor = 1,
Rollershadeexterior = 2,
Rollershadeexterior2motor = 3,
Drapery = 4,
Awning = 5,
Shutter = 6,
Tiltblindtiltonly = 7,
Tiltblindliftandtilt = 8,
Projectorscreen = 9,
Unknown = 255,
}
impl Type {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Type::Rollershade),
1 => Some(Type::Rollershade2motor),
2 => Some(Type::Rollershadeexterior),
3 => Some(Type::Rollershadeexterior2motor),
4 => Some(Type::Drapery),
5 => Some(Type::Awning),
6 => Some(Type::Shutter),
7 => Some(Type::Tiltblindtiltonly),
8 => Some(Type::Tiltblindliftandtilt),
9 => Some(Type::Projectorscreen),
255 => Some(Type::Unknown),
_ => None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
}
impl From<Type> for u8 {
fn from(val: Type) -> Self {
val as u8
}
}
pub type ConfigStatus = u8;
pub mod configstatus {
pub const OPERATIONAL: u8 = 0x01;
pub const ONLINE_RESERVED: u8 = 0x02;
pub const LIFT_MOVEMENT_REVERSED: u8 = 0x04;
pub const LIFT_POSITION_AWARE: u8 = 0x08;
pub const TILT_POSITION_AWARE: u8 = 0x10;
pub const LIFT_ENCODER_CONTROLLED: u8 = 0x20;
pub const TILT_ENCODER_CONTROLLED: u8 = 0x40;
}
pub type Mode = u8;
pub mod mode {
pub const MOTOR_DIRECTION_REVERSED: u8 = 0x01;
pub const CALIBRATION_MODE: u8 = 0x02;
pub const MAINTENANCE_MODE: u8 = 0x04;
pub const LED_FEEDBACK: u8 = 0x08;
}
pub type OperationalStatus = u8;
pub type SafetyStatus = u16;
pub mod safetystatus {
pub const REMOTE_LOCKOUT: u16 = 0x01;
pub const TAMPER_DETECTION: u16 = 0x02;
pub const FAILED_COMMUNICATION: u16 = 0x04;
pub const POSITION_FAILURE: u16 = 0x08;
pub const THERMAL_PROTECTION: u16 = 0x10;
pub const OBSTACLE_DETECTED: u16 = 0x20;
pub const POWER: u16 = 0x40;
pub const STOP_INPUT: u16 = 0x80;
pub const MOTOR_JAMMED: u16 = 0x100;
pub const HARDWARE_FAILURE: u16 = 0x200;
pub const MANUAL_OPERATION: u16 = 0x400;
pub const PROTECTION: u16 = 0x800;
}
pub fn encode_go_to_lift_percentage(lift_percent100ths_value: u16) -> anyhow::Result<Vec<u8>> {
let tlv = tlv::TlvItemEnc {
tag: 0,
value: tlv::TlvItemValueEnc::StructInvisible(vec![
(0, tlv::TlvItemValueEnc::UInt16(lift_percent100ths_value)).into(),
]),
};
Ok(tlv.encode()?)
}
pub fn encode_go_to_tilt_percentage(tilt_percent100ths_value: u16) -> anyhow::Result<Vec<u8>> {
let tlv = tlv::TlvItemEnc {
tag: 0,
value: tlv::TlvItemValueEnc::StructInvisible(vec![
(0, tlv::TlvItemValueEnc::UInt16(tilt_percent100ths_value)).into(),
]),
};
Ok(tlv.encode()?)
}
pub fn decode_type_(inp: &tlv::TlvItemValue) -> anyhow::Result<Type> {
if let tlv::TlvItemValue::Int(v) = inp {
Type::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
} else {
Err(anyhow::anyhow!("Expected Integer"))
}
}
pub fn decode_number_of_actuations_lift(inp: &tlv::TlvItemValue) -> anyhow::Result<u16> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(*v as u16)
} else {
Err(anyhow::anyhow!("Expected UInt16"))
}
}
pub fn decode_number_of_actuations_tilt(inp: &tlv::TlvItemValue) -> anyhow::Result<u16> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(*v as u16)
} else {
Err(anyhow::anyhow!("Expected UInt16"))
}
}
pub fn decode_config_status(inp: &tlv::TlvItemValue) -> anyhow::Result<ConfigStatus> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(*v as u8)
} else {
Err(anyhow::anyhow!("Expected Integer"))
}
}
pub fn decode_current_position_lift_percentage(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u8>> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(Some(*v as u8))
} else {
Ok(None)
}
}
pub fn decode_current_position_tilt_percentage(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u8>> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(Some(*v as u8))
} else {
Ok(None)
}
}
pub fn decode_operational_status(inp: &tlv::TlvItemValue) -> anyhow::Result<OperationalStatus> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(*v as u8)
} else {
Err(anyhow::anyhow!("Expected Integer"))
}
}
pub fn decode_target_position_lift_percent100ths(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u16>> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(Some(*v as u16))
} else {
Ok(None)
}
}
pub fn decode_target_position_tilt_percent100ths(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u16>> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(Some(*v as u16))
} else {
Ok(None)
}
}
pub fn decode_end_product_type(inp: &tlv::TlvItemValue) -> anyhow::Result<EndProductType> {
if let tlv::TlvItemValue::Int(v) = inp {
EndProductType::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
} else {
Err(anyhow::anyhow!("Expected Integer"))
}
}
pub fn decode_current_position_lift_percent100ths(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u16>> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(Some(*v as u16))
} else {
Ok(None)
}
}
pub fn decode_current_position_tilt_percent100ths(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u16>> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(Some(*v as u16))
} else {
Ok(None)
}
}
pub fn decode_mode(inp: &tlv::TlvItemValue) -> anyhow::Result<Mode> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(*v as u8)
} else {
Err(anyhow::anyhow!("Expected Integer"))
}
}
pub fn decode_safety_status(inp: &tlv::TlvItemValue) -> anyhow::Result<SafetyStatus> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(*v as u16)
} else {
Err(anyhow::anyhow!("Expected Integer"))
}
}
pub fn decode_attribute_json(cluster_id: u32, attribute_id: u32, tlv_value: &crate::tlv::TlvItemValue) -> String {
if cluster_id != 0x0102 {
return format!("{{\"error\": \"Invalid cluster ID. Expected 0x0102, got {}\"}}", cluster_id);
}
match attribute_id {
0x0000 => {
match decode_type_(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0005 => {
match decode_number_of_actuations_lift(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0006 => {
match decode_number_of_actuations_tilt(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0007 => {
match decode_config_status(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0008 => {
match decode_current_position_lift_percentage(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0009 => {
match decode_current_position_tilt_percentage(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x000A => {
match decode_operational_status(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x000B => {
match decode_target_position_lift_percent100ths(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x000C => {
match decode_target_position_tilt_percent100ths(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x000D => {
match decode_end_product_type(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x000E => {
match decode_current_position_lift_percent100ths(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x000F => {
match decode_current_position_tilt_percent100ths(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0017 => {
match decode_mode(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x001A => {
match decode_safety_status(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
_ => format!("{{\"error\": \"Unknown attribute ID: {}\"}}", attribute_id),
}
}
pub fn get_attribute_list() -> Vec<(u32, &'static str)> {
vec![
(0x0000, "Type"),
(0x0005, "NumberOfActuationsLift"),
(0x0006, "NumberOfActuationsTilt"),
(0x0007, "ConfigStatus"),
(0x0008, "CurrentPositionLiftPercentage"),
(0x0009, "CurrentPositionTiltPercentage"),
(0x000A, "OperationalStatus"),
(0x000B, "TargetPositionLiftPercent100ths"),
(0x000C, "TargetPositionTiltPercent100ths"),
(0x000D, "EndProductType"),
(0x000E, "CurrentPositionLiftPercent100ths"),
(0x000F, "CurrentPositionTiltPercent100ths"),
(0x0017, "Mode"),
(0x001A, "SafetyStatus"),
]
}
pub fn get_command_list() -> Vec<(u32, &'static str)> {
vec![
(0x00, "UpOrOpen"),
(0x01, "DownOrClose"),
(0x02, "StopMotion"),
(0x05, "GoToLiftPercentage"),
(0x08, "GoToTiltPercentage"),
]
}
pub fn get_command_name(cmd_id: u32) -> Option<&'static str> {
match cmd_id {
0x00 => Some("UpOrOpen"),
0x01 => Some("DownOrClose"),
0x02 => Some("StopMotion"),
0x05 => Some("GoToLiftPercentage"),
0x08 => Some("GoToTiltPercentage"),
_ => None,
}
}
pub fn get_command_schema(cmd_id: u32) -> Option<Vec<crate::clusters::codec::CommandField>> {
match cmd_id {
0x00 => Some(vec![]),
0x01 => Some(vec![]),
0x02 => Some(vec![]),
0x05 => Some(vec![
crate::clusters::codec::CommandField { tag: 0, name: "lift_percent100ths_value", kind: crate::clusters::codec::FieldKind::U32, optional: false, nullable: false },
]),
0x08 => Some(vec![
crate::clusters::codec::CommandField { tag: 0, name: "tilt_percent100ths_value", kind: crate::clusters::codec::FieldKind::U32, optional: false, nullable: false },
]),
_ => None,
}
}
pub fn encode_command_json(cmd_id: u32, args: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
match cmd_id {
0x00 => Ok(vec![]),
0x01 => Ok(vec![]),
0x02 => Ok(vec![]),
0x05 => {
let lift_percent100ths_value = crate::clusters::codec::json_util::get_u16(args, "lift_percent100ths_value")?;
encode_go_to_lift_percentage(lift_percent100ths_value)
}
0x08 => {
let tilt_percent100ths_value = crate::clusters::codec::json_util::get_u16(args, "tilt_percent100ths_value")?;
encode_go_to_tilt_percentage(tilt_percent100ths_value)
}
_ => Err(anyhow::anyhow!("unknown command ID: 0x{:02X}", cmd_id)),
}
}
pub async fn up_or_open(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<()> {
conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_CMD_ID_UPOROPEN, &[]).await?;
Ok(())
}
pub async fn down_or_close(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<()> {
conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_CMD_ID_DOWNORCLOSE, &[]).await?;
Ok(())
}
pub async fn stop_motion(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<()> {
conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_CMD_ID_STOPMOTION, &[]).await?;
Ok(())
}
pub async fn go_to_lift_percentage(conn: &crate::controller::Connection, endpoint: u16, lift_percent100ths_value: u16) -> anyhow::Result<()> {
conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_CMD_ID_GOTOLIFTPERCENTAGE, &encode_go_to_lift_percentage(lift_percent100ths_value)?).await?;
Ok(())
}
pub async fn go_to_tilt_percentage(conn: &crate::controller::Connection, endpoint: u16, tilt_percent100ths_value: u16) -> anyhow::Result<()> {
conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_CMD_ID_GOTOTILTPERCENTAGE, &encode_go_to_tilt_percentage(tilt_percent100ths_value)?).await?;
Ok(())
}
pub async fn read_type_(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Type> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_TYPE).await?;
decode_type_(&tlv)
}
pub async fn read_number_of_actuations_lift(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<u16> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_NUMBEROFACTUATIONSLIFT).await?;
decode_number_of_actuations_lift(&tlv)
}
pub async fn read_number_of_actuations_tilt(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<u16> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_NUMBEROFACTUATIONSTILT).await?;
decode_number_of_actuations_tilt(&tlv)
}
pub async fn read_config_status(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<ConfigStatus> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_CONFIGSTATUS).await?;
decode_config_status(&tlv)
}
pub async fn read_current_position_lift_percentage(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u8>> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_CURRENTPOSITIONLIFTPERCENTAGE).await?;
decode_current_position_lift_percentage(&tlv)
}
pub async fn read_current_position_tilt_percentage(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u8>> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_CURRENTPOSITIONTILTPERCENTAGE).await?;
decode_current_position_tilt_percentage(&tlv)
}
pub async fn read_operational_status(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<OperationalStatus> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_OPERATIONALSTATUS).await?;
decode_operational_status(&tlv)
}
pub async fn read_target_position_lift_percent100ths(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u16>> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_TARGETPOSITIONLIFTPERCENT100THS).await?;
decode_target_position_lift_percent100ths(&tlv)
}
pub async fn read_target_position_tilt_percent100ths(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u16>> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_TARGETPOSITIONTILTPERCENT100THS).await?;
decode_target_position_tilt_percent100ths(&tlv)
}
pub async fn read_end_product_type(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<EndProductType> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_ENDPRODUCTTYPE).await?;
decode_end_product_type(&tlv)
}
pub async fn read_current_position_lift_percent100ths(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u16>> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_CURRENTPOSITIONLIFTPERCENT100THS).await?;
decode_current_position_lift_percent100ths(&tlv)
}
pub async fn read_current_position_tilt_percent100ths(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u16>> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_CURRENTPOSITIONTILTPERCENT100THS).await?;
decode_current_position_tilt_percent100ths(&tlv)
}
pub async fn read_mode(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Mode> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_MODE).await?;
decode_mode(&tlv)
}
pub async fn read_safety_status(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<SafetyStatus> {
let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_WINDOW_COVERING, crate::clusters::defs::CLUSTER_WINDOW_COVERING_ATTR_ID_SAFETYSTATUS).await?;
decode_safety_status(&tlv)
}