Skip to main content

bacnet_objects/lighting/
color_link.rs

1//! The colour links of the two lighting output types (#1527,
2//! Addendum 135-2020ca part 4): Color_Reference, Color_Override and
3//! Override_Color_Reference.
4//!
5//! A lighting output sets its own luminance, but its colour comes from a
6//! companion colour object (Color or Color Temperature) in the same device:
7//! the one Color_Reference names or, while Color_Override is TRUE, the one
8//! Override_Color_Reference names (Clauses 12.54.X to 12.54.Z and 12.55.X to
9//! 12.55.Z). Turning the override on or off only changes which object the
10//! colour comes from. Neither the output's own fade nor the referenced
11//! object's is touched, so once the override ends the colour is wherever the
12//! Color_Reference object has got to by then. Nothing here writes a colour
13//! object: the outputs store and serve the references, and
14//! [`ObjectDatabase::lighting_color`] follows them to the colour shown.
15//!
16//! A reference names a colour object of either type. Instance 4194303 names
17//! none, whatever the type, which leaves the colour to the application: the
18//! clauses give the instance alone that meaning, so any type is taken with it
19//! for interoperability. A reference to an
20//! object the database doesn't hold is stored and served but not followed:
21//! the clauses put the companion in the same device, and an object
22//! identifier can't name another one.
23
24use bacnet_types::constructed::BACnetXyColor;
25use bacnet_types::enums::{ObjectType, PropertyIdentifier as P};
26use bacnet_types::error::Error;
27use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
28
29use crate::common;
30use crate::database::ObjectDatabase;
31use crate::property_metadata::{
32    PropertyConformance::Optional, PropertyMetadata, PropertyPresenceCondition,
33    PropertyWriteCapability::Always,
34};
35
36/// A lighting output's link to the colour objects that set its colour
37/// (Addendum 135-2020ca part 4, #1527).
38///
39/// Each reference must name a colour object, Color or Color Temperature, or
40/// have instance 4194303, which names none whatever its type.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct ColorLink {
43    /// Color_Reference: the object that sets the colour.
44    pub reference: ObjectIdentifier,
45    /// Color_Override and Override_Color_Reference, present when the output
46    /// supports colour override.
47    pub color_override: Option<ColorOverride>,
48}
49
50/// A lighting output's colour override.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct ColorOverride {
53    /// Color_Override: whether Override_Color_Reference sets the colour now.
54    pub active: bool,
55    /// Override_Color_Reference: the object that sets the colour while
56    /// overridden.
57    pub reference: ObjectIdentifier,
58}
59
60impl ColorLink {
61    /// A link to `reference` with no colour override.
62    pub const fn new(reference: ObjectIdentifier) -> Self {
63        Self {
64            reference,
65            color_override: None,
66        }
67    }
68
69    /// The reference that sets the colour now: Override_Color_Reference
70    /// while overridden, otherwise Color_Reference. `None` when it names no
71    /// object (instance 4194303).
72    pub fn active_reference(&self) -> Option<ObjectIdentifier> {
73        let reference = match self.color_override {
74            Some(ColorOverride {
75                active: true,
76                reference,
77            }) => reference,
78            _ => self.reference,
79        };
80        (reference.instance_number() != ObjectIdentifier::WILDCARD_INSTANCE).then_some(reference)
81    }
82
83    /// Check both references: VALUE_OUT_OF_RANGE unless each names a colour
84    /// object of either type or has instance 4194303.
85    pub(super) fn checked(self) -> Result<Self, Error> {
86        checked_reference(self.reference)?;
87        if let Some(color_override) = self.color_override {
88            checked_reference(color_override.reference)?;
89        }
90        Ok(self)
91    }
92}
93
94fn checked_reference(reference: ObjectIdentifier) -> Result<ObjectIdentifier, Error> {
95    if reference.instance_number() == ObjectIdentifier::WILDCARD_INSTANCE
96        || matches!(
97            reference.object_type(),
98            ObjectType::COLOR | ObjectType::COLOR_TEMPERATURE
99        )
100    {
101        Ok(reference)
102    } else {
103        Err(common::value_out_of_range_error())
104    }
105}
106
107/// The metadata rows `link` brings, in the order Tables 12-64 and 12-69 give
108/// them. Each is required while present (the tables' footnotes), and each
109/// takes writes: Color_Override must, and the references take one that
110/// names a colour object.
111pub(super) fn metadata(link: Option<&ColorLink>) -> impl Iterator<Item = PropertyMetadata> {
112    let overridable = link.is_some_and(|link| link.color_override.is_some());
113    [
114        (P::COLOR_REFERENCE, link.is_some()),
115        (P::COLOR_OVERRIDE, overridable),
116        (P::OVERRIDE_COLOR_REFERENCE, overridable),
117    ]
118    .into_iter()
119    .filter(|&(_, present)| present)
120    .map(|(property, _)| {
121        PropertyMetadata::new(
122            property,
123            Optional,
124            Some(PropertyPresenceCondition::LightingColor),
125            Always,
126        )
127    })
128}
129
130/// Read a colour-link property, if `property` is one; absent ones are
131/// UNKNOWN_PROPERTY.
132pub(super) fn read(link: Option<&ColorLink>, property: P) -> Option<Result<PropertyValue, Error>> {
133    let color_override = link.and_then(|link| link.color_override);
134    let value = match property {
135        P::COLOR_REFERENCE => link.map(|link| PropertyValue::ObjectIdentifier(link.reference)),
136        P::COLOR_OVERRIDE => color_override.map(|o| PropertyValue::Boolean(o.active)),
137        P::OVERRIDE_COLOR_REFERENCE => {
138            color_override.map(|o| PropertyValue::ObjectIdentifier(o.reference))
139        }
140        _ => return None,
141    };
142    Some(value.ok_or_else(common::unknown_property_error))
143}
144
145/// Write a colour-link property, if `property` is one and present.
146pub(super) fn write(
147    link: &mut Option<ColorLink>,
148    property: P,
149    value: &PropertyValue,
150) -> Option<Result<(), Error>> {
151    if let Err(error) = read(link.as_ref(), property)? {
152        return Some(Err(error));
153    }
154    let link = link.as_mut()?;
155    Some(match (property, value) {
156        (P::COLOR_REFERENCE, PropertyValue::ObjectIdentifier(reference)) => {
157            checked_reference(*reference).map(|reference| link.reference = reference)
158        }
159        (P::COLOR_OVERRIDE, PropertyValue::Boolean(active)) => {
160            if let Some(color_override) = &mut link.color_override {
161                color_override.active = *active;
162            }
163            Ok(())
164        }
165        (P::OVERRIDE_COLOR_REFERENCE, PropertyValue::ObjectIdentifier(reference)) => {
166            checked_reference(*reference).map(|reference| {
167                if let Some(color_override) = &mut link.color_override {
168                    color_override.reference = reference;
169                }
170            })
171        }
172        _ => Err(common::invalid_data_type_error()),
173    })
174}
175
176/// The colour link `output` serves through its properties, if it has one.
177fn read_link(output: &dyn crate::traits::BACnetObject) -> Option<ColorLink> {
178    let read = |property| output.read_property(property, None).ok();
179    let Some(PropertyValue::ObjectIdentifier(reference)) = read(P::COLOR_REFERENCE) else {
180        return None;
181    };
182    let color_override = match (read(P::COLOR_OVERRIDE), read(P::OVERRIDE_COLOR_REFERENCE)) {
183        (
184            Some(PropertyValue::Boolean(active)),
185            Some(PropertyValue::ObjectIdentifier(reference)),
186        ) => Some(ColorOverride { active, reference }),
187        _ => None,
188    };
189    Some(ColorLink {
190        reference,
191        color_override,
192    })
193}
194
195/// The colour a lighting output shows, from the colour object it follows.
196#[derive(Debug, Clone, Copy, PartialEq)]
197pub struct LightingColor {
198    /// The colour object the colour comes from.
199    pub source: ObjectIdentifier,
200    /// That object's Tracking_Value.
201    pub color: OutputColor,
202}
203
204/// A colour as a colour object's Tracking_Value gives it.
205#[derive(Debug, Clone, Copy, PartialEq)]
206pub enum OutputColor {
207    /// A Color object's CIE 1931 xy colour.
208    Xy(BACnetXyColor),
209    /// A Color Temperature object's correlated colour temperature, in
210    /// kelvin.
211    Kelvin(u32),
212}
213
214impl ObjectDatabase {
215    /// The colour the lighting output `lighting_output` shows now (#1527):
216    /// the Tracking_Value of the colour object its Color_Reference names, or
217    /// its Override_Color_Reference while Color_Override is TRUE, so a
218    /// colour fade in progress shows as it runs.
219    ///
220    /// `None` when the object has no Color_Reference, the reference in use
221    /// names no object (instance 4194303), or this database holds no Color
222    /// or Color Temperature object by that identifier. It reads the
223    /// properties, so it follows any object that serves them.
224    pub fn lighting_color(&self, lighting_output: &ObjectIdentifier) -> Option<LightingColor> {
225        let source = read_link(self.get(lighting_output)?)?.active_reference()?;
226        let tracking = self
227            .get(&source)?
228            .read_property(P::TRACKING_VALUE, None)
229            .ok()?;
230        let color = match (source.object_type(), tracking) {
231            (ObjectType::COLOR, PropertyValue::List(xy)) => match xy.as_slice() {
232                [PropertyValue::Real(x), PropertyValue::Real(y)] => {
233                    OutputColor::Xy(BACnetXyColor::new(*x, *y))
234                }
235                _ => return None,
236            },
237            (ObjectType::COLOR_TEMPERATURE, PropertyValue::Unsigned(kelvin)) => {
238                OutputColor::Kelvin(u32::try_from(kelvin).ok()?)
239            }
240            _ => return None,
241        };
242        Some(LightingColor { source, color })
243    }
244}