use super::*;
use crate::traits::BACnetObject;
use bacnet_types::constructed::BACnetColorCommand;
use bacnet_types::enums::{ColorOperation as Op, ErrorClass, ErrorCode, PropertyIdentifier};
const D65: BACnetXyColor = BACnetXyColor::new(0.3127, 0.3290);
const CC: PropertyIdentifier = PropertyIdentifier::COLOR_COMMAND;
fn assert_error<T: std::fmt::Debug>(result: Result<T, Error>, expected: ErrorCode) {
match result {
Err(Error::Protocol { class, code }) => {
assert_eq!(class, ErrorClass::PROPERTY.to_raw() as u32);
assert_eq!(code, expected.to_raw() as u32, "expected {expected:?}");
}
other => panic!("expected PROPERTY/{expected:?}, got {other:?}"),
}
}
fn command(operation: Op) -> BACnetColorCommand {
BACnetColorCommand::new(operation)
}
fn octets(value: &[u8]) -> PropertyValue {
PropertyValue::ApplicationData(value.to_vec())
}
const FADE_TO_COLOR: [u8; 17] = [
0x09, 0x01, 0x1E, 0x44, 0x3F, 0x00, 0x00, 0x00, 0x44, 0x3E, 0x80, 0x00, 0x00, 0x1F, 0x3A, 0x07,
0xD0,
];
const FADE_TO_CCT: [u8; 7] = [0x09, 0x02, 0x2A, 0x0A, 0x8C, 0x39, 0x64];
fn fade_to_color() -> BACnetColorCommand {
BACnetColorCommand {
target_color: Some(BACnetXyColor::new(0.5, 0.25)),
fade_time: Some(2_000),
..command(Op::FADE_TO_COLOR)
}
}
#[test]
fn color_commands_start_at_none_and_serve_what_was_written() {
let mut color = ColorObject::new(1, "CLR-1").unwrap();
assert_eq!(color.color_command(), command(Op::NONE));
assert_eq!(
color.read_property(CC, None).unwrap(),
octets(&[0x09, 0x00])
);
color
.write_property(CC, None, octets(&FADE_TO_COLOR), None)
.unwrap();
assert_eq!(
color.read_property(CC, None).unwrap(),
octets(&FADE_TO_COLOR)
);
assert_eq!(color.color_command(), fade_to_color());
color.set_color_command(command(Op::STOP)).unwrap();
assert_eq!(
color.read_property(CC, None).unwrap(),
octets(&[0x09, 0x06])
);
assert_eq!(
color
.read_property(PropertyIdentifier::PRESENT_VALUE, None)
.unwrap(),
xy_value(D65)
);
let mut temperature = ColorTemperatureObject::new(1, "CT-1").unwrap();
assert_eq!(temperature.color_command(), command(Op::NONE));
assert_eq!(
temperature.read_property(CC, None).unwrap(),
octets(&[0x09, 0x00])
);
temperature
.write_property(CC, None, octets(&FADE_TO_CCT), None)
.unwrap();
assert_eq!(
temperature.read_property(CC, None).unwrap(),
octets(&FADE_TO_CCT)
);
assert_eq!(
temperature.color_command(),
BACnetColorCommand {
target_color_temperature: Some(2_700),
fade_time: Some(100),
..command(Op::FADE_TO_CCT)
}
);
assert_eq!(
temperature
.read_property(PropertyIdentifier::PRESENT_VALUE, None)
.unwrap(),
PropertyValue::Unsigned(2_700)
);
assert_eq!(
temperature
.read_property(PropertyIdentifier::IN_PROGRESS, None)
.unwrap(),
PropertyValue::Enumerated(1)
);
}
#[test]
fn color_commands_refuse_other_datatypes_and_broken_encodings() {
let objects: [Box<dyn BACnetObject>; 2] = [
Box::new(ColorObject::new(1, "CLR-1").unwrap()),
Box::new(ColorTemperatureObject::new(1, "CT-1").unwrap()),
];
for mut object in objects {
for value in [
PropertyValue::OctetString(FADE_TO_CCT.to_vec()),
PropertyValue::Unsigned(1),
PropertyValue::List(vec![octets(&FADE_TO_CCT[..2]), octets(&FADE_TO_CCT[2..])]),
octets(&[0x21, 0x01]),
octets(&FADE_TO_CCT[2..]),
] {
assert_error(
object.write_property(CC, None, value, None),
ErrorCode::INVALID_DATA_TYPE,
);
}
let trailing = [&FADE_TO_CCT[..], &[0x00]].concat();
let wide_then_trailing = [0x09, 0x02, 0x2D, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00];
for broken in [
&[][..],
&[0x09][..],
&FADE_TO_COLOR[..13],
&trailing[..],
&wide_then_trailing[..],
] {
assert_error(
object.write_property(CC, None, octets(broken), None),
ErrorCode::INVALID_DATA_ENCODING,
);
}
assert_error(
object.write_property(CC, None, octets(&wide_then_trailing[..9]), None),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(
object.read_property(CC, None).unwrap(),
octets(&[0x09, 0x00])
);
}
}
#[test]
fn color_object_takes_fade_to_color_and_stop_only() {
let target = |x, y| BACnetColorCommand {
target_color: Some(BACnetXyColor::new(x, y)),
..command(Op::FADE_TO_COLOR)
};
let refused = [
command(Op::NONE),
BACnetColorCommand {
target_color_temperature: Some(2_700),
..command(Op::FADE_TO_CCT)
},
BACnetColorCommand {
target_color_temperature: Some(2_700),
..command(Op::RAMP_TO_CCT)
},
command(Op::STEP_UP_CCT),
command(Op::STEP_DOWN_CCT),
command(Op::from_raw(7)),
command(Op::from_raw(u32::MAX)),
command(Op::FADE_TO_COLOR),
target(-0.1, 0.5),
target(0.5, 1.5),
target(f32::NAN, 0.5),
target(0.5, f32::INFINITY),
BACnetColorCommand {
fade_time: Some(99),
..target(0.5, 0.5)
},
BACnetColorCommand {
fade_time: Some(86_400_001),
..target(0.5, 0.5)
},
];
let mut color = ColorObject::new(1, "CLR-1").unwrap();
for value in refused {
assert_error(
color.set_color_command(value),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(color.color_command(), command(Op::NONE), "{value:?}");
}
let accepted = [
BACnetColorCommand {
fade_time: Some(100),
..target(0.0, 0.0)
},
BACnetColorCommand {
fade_time: Some(86_400_000),
..target(1.0, 1.0)
},
target(0.3127, 0.3290),
command(Op::STOP),
BACnetColorCommand {
target_color_temperature: Some(0),
ramp_rate: Some(0),
step_increment: Some(u32::MAX),
..target(0.5, 0.5)
},
BACnetColorCommand {
target_color: Some(BACnetXyColor::new(9.0, -9.0)),
fade_time: Some(1),
..command(Op::STOP)
},
];
for value in accepted {
color.set_color_command(value).unwrap();
assert_eq!(color.color_command(), value);
}
}
#[test]
fn color_temperature_object_takes_the_cct_operations_and_stop() {
let target = |operation, kelvin| BACnetColorCommand {
target_color_temperature: Some(kelvin),
..command(operation)
};
let refused = [
command(Op::NONE),
BACnetColorCommand {
target_color: Some(BACnetXyColor::new(0.5, 0.5)),
..command(Op::FADE_TO_COLOR)
},
command(Op::from_raw(7)),
command(Op::from_raw(255)),
command(Op::FADE_TO_CCT),
command(Op::RAMP_TO_CCT),
target(Op::FADE_TO_CCT, 999),
target(Op::RAMP_TO_CCT, 30_001),
target(Op::FADE_TO_CCT, 0),
BACnetColorCommand {
fade_time: Some(99),
..target(Op::FADE_TO_CCT, 2_700)
},
BACnetColorCommand {
fade_time: Some(86_400_001),
..target(Op::FADE_TO_CCT, 2_700)
},
BACnetColorCommand {
ramp_rate: Some(0),
..target(Op::RAMP_TO_CCT, 2_700)
},
BACnetColorCommand {
ramp_rate: Some(30_001),
..target(Op::RAMP_TO_CCT, 2_700)
},
BACnetColorCommand {
step_increment: Some(0),
..command(Op::STEP_UP_CCT)
},
BACnetColorCommand {
step_increment: Some(30_001),
..command(Op::STEP_DOWN_CCT)
},
];
let mut temperature = ColorTemperatureObject::new(1, "CT-1").unwrap();
for value in refused {
assert_error(
temperature.set_color_command(value),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(temperature.color_command(), command(Op::NONE), "{value:?}");
}
let accepted = [
BACnetColorCommand {
fade_time: Some(100),
..target(Op::FADE_TO_CCT, 1_000)
},
BACnetColorCommand {
fade_time: Some(86_400_000),
..target(Op::FADE_TO_CCT, 30_000)
},
BACnetColorCommand {
ramp_rate: Some(1),
..target(Op::RAMP_TO_CCT, 30_000)
},
BACnetColorCommand {
ramp_rate: Some(30_000),
..target(Op::RAMP_TO_CCT, 1_000)
},
BACnetColorCommand {
step_increment: Some(1),
..command(Op::STEP_UP_CCT)
},
BACnetColorCommand {
step_increment: Some(30_000),
..command(Op::STEP_DOWN_CCT)
},
command(Op::STEP_UP_CCT),
command(Op::STOP),
target(Op::FADE_TO_CCT, 1_000),
BACnetColorCommand {
target_color: Some(BACnetXyColor::new(-1.0, 2.0)),
ramp_rate: Some(0),
step_increment: Some(0),
..target(Op::FADE_TO_CCT, 2_700)
},
BACnetColorCommand {
target_color_temperature: Some(0),
fade_time: Some(0),
..command(Op::STEP_DOWN_CCT)
},
];
temperature.set_min_max(2_000, 6_500).unwrap();
for value in accepted {
temperature.set_color_command(value).unwrap();
assert_eq!(temperature.color_command(), value);
}
}
#[test]
fn color_objects_no_longer_answer_to_508_to_511() {
let objects: [Box<dyn BACnetObject>; 2] = [
Box::new(ColorObject::new(1, "CLR-1").unwrap()),
Box::new(ColorTemperatureObject::new(1, "CT-1").unwrap()),
];
for object in objects {
for raw in [508, 509, 510, 511] {
let property = PropertyIdentifier::from_raw(raw);
assert!(!object.property_list().contains(&property));
assert_error(
object.read_property(property, None),
ErrorCode::UNKNOWN_PROPERTY,
);
}
let list = object.property_list();
assert!(list.contains(&PropertyIdentifier::from_raw(4_194_334)));
assert!(object
.read_property(PropertyIdentifier::from_raw(4_194_334), None)
.is_ok());
}
let color = ColorObject::new(1, "CLR-1").unwrap();
assert_eq!(
color
.read_property(PropertyIdentifier::from_raw(4_194_330), None)
.unwrap(),
xy_value(D65)
);
let temperature = ColorTemperatureObject::new(1, "CT-1").unwrap();
assert_eq!(
temperature
.read_property(PropertyIdentifier::from_raw(4_194_331), None)
.unwrap(),
PropertyValue::Unsigned(4000)
);
}