BrickSchema / Brick

Uniform metadata schema for buildings
http://brickschema.org/
BSD 3-Clause "New" or "Revised" License
290 stars 79 forks source link

enumerations as abstractions for control #566

Open epaulson opened 11 months ago

epaulson commented 11 months ago

This is a big can of worms, but we should have a better answer for "how do I effect a change in a building using metadata I find in Brick" - either by extending Brick or finding additional standards to bridge the gap. Our solution today is not very complete.

We have a good end-to-end story for reading data for a building and some simple controls. For example, a temperature sensor and fan on/off: (in pseudo-Brick, I was typing fast and just making stuff up, this isn't 100% correct)

ex:room101TempPoint a brick:Temperature_Sensor; 
     brick:isPointOf ex:room101;
     brick:hasUnit qudt:F ;
     brick:hasExternalRef ex:r101bacnet .

ex:101bacnet a ref:BACnetReference ;
    bacnet:objectType bacnet:analogInput ;
    bacnet:bacnetDeviceId 599;
    bacnet:instanceNum 1 ;
    bacnet:property 'present-value' .

ex:fan001 a brick:Fan .
ex:fan001_on_off a brick:On_Off_Command ;
     brick:isPointOf ex:fan001 ;
     brick:hasExternalRef ex:fan001bacnet .

ex:fan001bacnet a ref:BACnetReference;
  brick:BACnetURI "bacnet://599/binary-output,3/present-value" ;

That's enough metadata so a software package that knows how to read both Brick and speak BACnet can read the temperature of a room and turn a fan off and on. We even tell it that the temperature is measured in fahrenheit.

Implicitly we expect the software author to know that for reading the temperature, they should invoke the BACnet device's read_property service and that the presentValue of an analog Input is going to be a float. We also expect them to recognize that a for an On_Off_Command that's attached to a Binary_Output BACnet object, reading or overriding that to 'True' means the fan should start, and overriding the object to be 'False' means the fan should stop running.

This is going to sound a bit dismissive, but these are sort of the 'trivial' operations - but I think for the 'trivial' (but very common!) scenarios where we want to go end-to-end in reading/interpreting the present status of a device, or changing that state, we have enough metadata in a Brick model that a piece of software or a device that has access to the Brick model and access to the BACnet network can out-of-the box do what people expect.

But there are some things that I'd call non-trivial, and in those places I think we run into trouble. I want to highlight one - multi-state objects. Consider these examples. taken from the Chipkin docs ( https://store.chipkin.com/articles/bacnet-what-is-a-bacnet-multi-state-object )

Multi-state Input Object Example of a Two-Speed Fan:

Object_Identifier = (Multi-state Input, Instance 1)
Object_Name = "Fan1_Input"
Object_Type = MULTI_STATE_INPUT
Present_Value = 2
Description = "2-speed Fan#1"
Device_Type = "ZZZ Fan Motor"
Status_Flags = {FALSE, FALSE, FALSE, FALSE}
Event_State = NORMAL
Reliability = NO_FAULT_DETECTED
Out_Of_Service = FALSE
Number_Of_States = 3
State_Text = ("Off", "On_Low", "On_High")
Time_Delay = 3
Notification_Class = 4
Alarm_Values = (3)
Fault_Values = (2)
Event_Enable = {TRUE, TRUE, TRUE}
Acked_Transitions = {TRUE, TRUE, TRUE}
Notify_Type = EVENT
Event_Time_Stamps = ((23-MAR-95,18:50:21.2), (*-*-*,*:*:*.*), (23-MAR-95,19:01:34.0))

Multi-state Output Object Example of a Three Position Switch:

Object_Identifier = (Multi-state Output, Instance 2)
Object_Name = "3-POS-SW"
Object_Type = MULTI_STATE_OUTPUT
Present_Value = 1
Description = "3 POSITION SWITCH #1"
Status_Flags = {FALSE, FALSE, FALSE, FALSE}
Event_State = NORMAL
Reliability = NO_FAULT_DETECTED
Out_Of_Service = FALSE
Number_Of_States = 3
State_Text = {"Position_1", "Position_2", "Position_3"}
Priority_Array = {NULL...NULL}
Relinquish_Default = 1

Multi-state Value Object Example of a Ventilator Unit Operational Mode:

Object_Identifier = (Multi-state Value, Instance 1)
Object_Name = "UV39"
Object_Type = MULTISTATE_VALUE
Present_Value = 2
Description = "UnitVent Room 39"
Status_Flags = {FALSE,FALSE,FALSE,FALSE}
Event_State = NORMAL
Reliability = NO_FAULT_DETECTED
Out_Of_Service = FALSE
Number_Of_States = 4
State_Text = ("Unoccupied","Warmup","Occupied","Setback")

A tool that wanted to change the state of that room vent, for example, is not going to have enough semantically-useful information to know what to read or write to the BACnet object without finding some additional metadata.

I think we should do two things in Brick to help with these sorts of non-trivial points:

  1. Add some standard enumerations to Brick, or adopt someone else's standard enumerations, for common states such as Occupied/Unoccupied/Warmup/Precool/On/Off/Fast/Slow/High/Low/etc etc etc
  2. Something like the ref schema so a local site can "bind" the standard enumerations to the local object configurations.

For example, using UV39 above

ex:room39vent_state a brick:Mode_Status ;
     brick:isPointOf ex:vent0039 ;
     brick:hasExternalRef ex:vent0039bacnet .

ex:vent0039bacnet a ref:BACnetReference;
  brick:BACnetURI "bacnet://599/Multi-state-value,1/present-value" ;
  brick:BACnetStateMapping [ brick:UnoccupiedEnum brick:hasLocalState 0 ;
                                                    brick:WarmupPeriodEnum brick:hasLocalState 1;
                                                    brick:OccupiedEnum brick:hasLocalState 2; 
                                                    brick:LocallyDefinedEnum brick:hasLocalState 3 .]

So this adds extra work to creating a Brick model for a site - before, where a modeler "only" had to identify the equipment, points, and spaces of a building, name them, assign appropriate classes to them, and create isPart/isLocation/isPointOf relationships between them and also create a connection from the Brick instances to the BACnet external references, now they will also have to map any local 'state' types and map them to the 'standard' states.

There is, alas, no complete set of standard state enumerations, especially not one where everyone agrees on the bitwise serialization. (There are a lot of enumerations in BACnets - see some of them in the BACnet ttl starting at bacnet:AuditOperation.acknowledge-alarm or see this list - https://docs.iconics.com/V10.96.2/GENESIS64/Help/Apps/WBDT/BACnet/BACnet_Enumerated_Data_Types.htm ) - so those can be a good starting point, but I don't think they're got nearly enough coverage for most building operations.

This local binding of "non-trivial values" to points seems like it has to be a problem that CDL/ASHRAE 231 are grappling with as well. Imagine you want to implement a G36 sequence written in CDL on an already existing site that has a set of valves or fans or actuators or what have you deployed on their BACnet network. When the CDL is compiled/translated from Modelica into the local control language, e.g. EIKON, where does the compiler get the information it needs to know what integers to write into the status point/register when it wants a fan to start or stop? Those integers were assigned by the technician who came to the building and programmed the BMS, and it's a coin flip if they used HIGH=1, LOW=2 or HIGH=2, LOW=1.

A related problem is anything that has some kind of percentage or some kind of level - does Brightness go from 1 to 100 or from 1 to 10? On a window tint, does 0 mean 'let all light in' or 'let no light in'? But that might be a problem for another day.

gtfierro commented 11 months ago

@JoelBender @anandkp92 I wonder if you have any thoughts on this? It seems like the kind of thing that 231/CDL will have to deal with as well

anandkp92 commented 11 months ago

So 231/CDL deals with only the control sequence, that is, it assumes that "something else" will do the mappings between the real hardware and the control sequence. In our current examples, we have assumed that the bacnet info will be present in the semantic model (Brick/223) and we're working on tools to do this mapping between a semantic model and the control sequence.

That said, CDL/231 does have a "enumeration type" that we could use.

type E = enumeration([enumList]);

Example:

type SimpleController = enumeration( 
    P "P controller", 
    PI "PI controller", 
    PD "PD controller", 
    PID "PID controller") 
"Enumeration defining P, PI, PD, or PID simple controller type";

And a clarification question: In the multi-state examples above, are the states represented in BACnet as ("Off", "On_Low", "On_High") or as integer representations as (1,23)?

If its the former, using CDL enumeration types, we could create those enumerations "Off", "On_Low" and "On_High" for that point. However, I'm not sure this solves the problem of assign integer values to enumerations HIGH = 1, LOW =2? I believe it would be vendor specific. If we were to translate CDL <-> Eikon, the CDL-Eikon translator would incorporate the mapping between the HIGH/1 and LOW/2.

epaulson commented 11 months ago

However, I'm not sure this solves the problem of assign integer values to enumerations HIGH = 1, LOW =2? I believe it would be vendor specific.

Unfortunately, it's worse than being vendor-specific - the values in a multi-state point are going to be site-specific, or actually could vary from one device to the next, depending on who set them up.

For a multi-state BACnet object, to actually set a state, you write an integer into the 'Present_Value' property of the object. There is a 'Number_Of_States' property to tell you how many of them there might be, and you can use the 'State_Text' property to tell you the set of them and which position corresponds to which state.

However - what text is used in the 'State_Text' property is completely up to the person who installed and configured that device - one person might use 'Ready' and 'NotReady' and another may use 'Active' and 'Inactive' - totally up to the HVAC tech who installed and programmed the device. The BACnet book used another good example for a fan - it might be 'Off', 'ManualControl' or 'AutomaticControl'. Which integer which is which depends entirely on the order the programmer set them up. Hopefully 'Off' is 0 but which should come next, Manual or Automatic?

So we can't rely on the BACnet standard to give you the set of and meanings of the integers used as input to a Multistate point as any sort of standard. We also can't rely on the BACnet standard for the set of possible values in 'State_Text' and what their semantic meanings should be.

So if a portable, best-of-breed 231 sequence wants to turn a fan on, I think it needs to have some unambiguous sentinel value for what it means to turn a fan on, and then in the site specific metadata (or even probably device-specific) there needs to be a translation table that says '231_FAN_ON' (or 'BRICK_FAN'ON') corresponds to '1' for BACnet object XXX on device YYY, and the translated code needs to know where to find that metadata, either at runtime or at translation time, depending on how fancy you want your system to be.

I don't think it matters too much if it's Brick or 231 that define those concepts. It probably shouldn't be BACnet so 231 and Brick can be portable over protocols.

(In other crazy BACnet things, Binary Input objects have a 'Polarity' property, so you can set the 'Present_Value' to be 'Active' but if the 'Polarity' property is 'Reverse' then that usually means the device isn't running. As they say at Perl conferences, 'There's more than one way to do it!')

JoelBender commented 11 months ago

So if a portable, best-of-breed 231 sequence wants to turn a fan on, I think it needs to have some unambiguous sentinel value for what it means to turn a fan on.

I agree, and only CDL can define the scope/labels of what those identifiers are because it has to be explicit in what it can accept as an input and what it will provide as an output. 223 does this with enumeration kinds like RunStatus with "named individuals" (specific URIs), names like RunStatus-On and BACnet/RDF does this with DoorStatus.opened and provides the integer serialization value, but it's limited to specific "applications" (from the Applications Working Group, the 20-year precursor to the SI-WG).

If CDL enumerated values were always going to be mapped to Multi-state Inputs, Outputs and Values, it would be pretty simple to just state that the mapping shall be one-to-one. To use CDL sequences you'll be expected translate what it means to be in one of those states into real-world activity (some BO becomes active, or some other BO becomes inactive because it's inverted). If the State_Text array is present (in BACnet it's optional) then it would have to be up to a human to decide if that matches the label of the CDL definition.

This really only applies to CDL inputs and outputs; parameters (like the SimpleController described above) and constants are strictly within the domain of CDL. @anandkp92 if is there an expectation that these values like P, PI, etc., are to be mapped to some protocol then I would bring this up with the BACnet committee and maybe we can specify that the mapping will be Integer, Positive Integer (a.k.a. unsigned), or Multi-state.

JoelBender commented 11 months ago

@anandkp92 231 could say that enumeration values are always unsigned values in CDX, which would make it easier to exchange with other CDX users like BACnet enumerations. The value-to-meaning translation becomes part of the "schema" for the enumeration, i.e., cdx:SimpleController.P cdx:value 0 ; rdfs:label "P" ; rdfs:comment "P controller". That would help inform the BACnet committee the intent of these inputs and outputs. If there was a vendor that can only support Analog-like value objects, that would also be acceptable (at least for values in the range of enumerations).

david-waterworth commented 10 months ago

@epaulson that's exactly my experience, you cannot rely on 'State_Text' as it's optional, and the same concept can be surfaced many different ways (i.e. a boolean that indicates if a VAV should in included in a calculation could be a binary input or value "IncludeInCalculation" (0/1), an inverted binary input/value "ExcludeFromCalculation" (1/0) or a MultiStateInput/Value (True=1, False=2 or False=1, True=2) either defined as "Include" or "Exclude".

jbkoh commented 8 months ago

Late to chime in, it seems most of us agree that we need some modeling of the state text enumerations. In some cases, state texts can be not comprehensive, but at least we need a way to utilize what people have encoded in state texts. Furthermore, as Erik briefly mentioned, I think binary objects have the same issue as the states for true or false are very implicit. I think we should treat them in an equal way.

I like Erik's initial proposal to introduce a state value map. I was planning to do the same thing at my company and did initial research on the possible values for the enums.

A challenge is there will be no "end state" of designing our enumerated values as a state can be very cryptic to a certain device and a control designer. But I concluded personally that having something partial would be better than nothing. So, the standard enumeration mapping for a specific instance can be partial, but I'd just allow it and incrementally improve our vocabulary.

Another thing that I was worried is whether a certain enumeration value can mean different things in different contexts, which will need namespaces for each enum. E.g., does "on" mean the same thing in all objects? From the data I've seen, the majority of the enum values can be globally reusable, but I'd be happy to learn more from others.

Three things to standardize

Happy to provide the raw data to start modeling those and drive the implementation if there is some agreement.