JoelBender / BACpypes3

BACnet communications library
49 stars 10 forks source link

Proprietary objects registration #16

Closed ChristianTremblay closed 3 months ago

ChristianTremblay commented 1 year ago

What is the best way to register proprietray objects for a specific vendor ?

Let say I have a manufacturer vendor id and the implementation details on their object. In bacpypes, there was register_object_type

In bp3, I see a similar register_object_class but part of the VendorInfo class. Should I create a VendorInfo class ?

Or should I share what I have so it's part of bp3 as those infos are public ?

ChristianTremblay commented 1 year ago

I saw the custom example which looks interesting. Even if, I think it is meant for local objects (served by bacpypes app).

I think it would be nice to have something similar to load an existing vendor (or create if not in cache) to specify proprietary objects and properties.

# extract from what I would do to extend JCI objects
class ProprietaryPropertyIdentifier(PropertyIdentifier):
    """
    This is a list of the property identifiers that are used in custom object
    types or are used in custom properties of standard types.
    """
    # AV, AI, AO
    FLOW_SP_EEPROM = 3113
    Offset = 956
    Offline = 913
    SABusAddr = 3645
    PeerToPeer = 748
    P2P_ErrorStatus = 746
    InputRangeLow = 1293
    InputRangeHigh = 1294
    OutputRangeLow = 1295
    OutputRangeHigh = 1296
    MIN_OUT_VALUE = 652
    MAX_OUT_VALUE = 653
    #polarity = polarity
    stroketime = 3478

class AnalogInputObject(_AnalogInputObject):
    Offset: Real
    Offline: Boolean
    SABusAddr: Unsigned
    InputRangeLow: Real
    InputRangeHigh: Real
    OutputRangeLow: Real
    OutputRangeHigh: Real

class AnalogValueObject(_AnalogValueObject):
    FLOW_SP_EEPROM: Real
    Offset: Real
    Offline: Boolean
    SABusAddr: Unsigned
    PeerToPeer: Atomic
    P2P_ErrorStatus: Enumerated

class AnalogOutputObejt(_AnalogOutputObject):
    Offline: Boolean
    SABusAddr: Unsigned
    MIN_OUT_VALUE: Real
    MAX_OUT_VALUE: Real
    #polarity = polarity
    stroketime: Real
JoelBender commented 1 year ago

I saw the custom example which looks interesting. Even if, I think it is meant for local objects (served by bacpypes app).

It's for both clients and servers. For clients the module should import from the "vanilla" classes like AnalogValueObject from the bacpyes3.object module. For servers it inherits from the "local" classes to get the default behavior. The sample client application happens to not create instances of the objects which is a bit of a dodge.

I think it would be nice to have something similar to load an existing vendor (or create if not in cache) to specify proprietary objects and properties.

If the vendor wanted to distribute a Python module for their definitions that would be awesome, and a bit surprising! It's more likely that they would publish a CSML document, which could then be translated into Python and RDF classes. If you know any existing CSML documents we could try, double check the licensing for public consumption and we'll work on that.

ChristianTremblay commented 3 months ago

I'm working with this : https://github.com/ChristianTremblay/BAC0/blob/async/BAC0/core/proprietary_objects/jci_5.py It is based on a pdf I found in the past...on their public web site

ChristianTremblay commented 3 months ago
>>> import asyncio
>>> import BAC0
>>> bacnet = BAC0.lite()
[08/09/24 23:58:51] INFO     2024-08-09 23:58:51,428 - INFO    | Starting Asynchronous BAC0 version 2024 (Lite)                                                            notes.py:272
                    INFO     2024-08-09 23:58:51,434 - INFO    | Using bacpypes3 version 0.0.98                                                                            notes.py:272
                    INFO     2024-08-09 23:58:51,436 - INFO    | Use BAC0.log_level to adjust verbosity of the app.                                                        notes.py:272
                    INFO     2024-08-09 23:58:51,439 - INFO    | Ex. BAC0.log_level('silence') or BAC0.log_level('error')                                                  notes.py:272
                    INFO     2024-08-09 23:58:51,896 - INFO    | Using ip : 192.168.211.208/24 on port 47808 | broadcast : 192.168.211.255                                  Lite.py:168
[08/09/24 23:58:52] INFO     2024-08-09 23:58:52,187 - INFO    | Using JSON Stored in user folder ~/.BAC0                                                                  notes.py:272
[08/09/24 23:58:52] INFO     2024-08-09 23:58:52,199 - INFO    | Registered as BACnet/IP App                                                                               notes.py:272
                    INFO     2024-08-09 23:58:52,207 - INFO    | Device instance (id) : 3056381                                                                            notes.py:272
>>> cgm = BAC0.device('303:5', 5705, bacnet) 
[08/09/24 23:59:13] INFO     2024-08-09 23:59:13,911 - INFO    | Changing device state to DeviceDisconnected'>                                                             notes.py:272
>>> [08/09/24 23:59:14] INFO     2024-08-09 23:59:14,283 - INFO    | Changing device state to RPMDeviceConnected'>                                                             notes.py:272
                    INFO     2024-08-09 23:59:14,472 - INFO    | Device 5705:[CGM-7-005] found... building points list                                                    Device.py:537
[08/09/24 23:59:19] INFO     2024-08-09 23:59:19,865 - INFO    | Points and trendlogs (if any) created                                                                     notes.py:272
[08/09/24 23:59:19] INFO     2024-08-09 23:59:19,866 - INFO    | Device defined for normal polling with a delay of 10sec                                                    Poll.py:179
                    INFO     2024-08-09 23:59:19,868 - INFO    | Polling started, values read every 10 seconds                                                             notes.py:272
                    INFO     2024-08-09 23:59:19,869 - INFO    | Device ready, use device_name.points and start interact with it                                          Device.py:551
await cgm['DA-T'].update_bacnet_properties() 
>>> await cgm['DA-T'].bacnet_properties         
{
    <PropertyIdentifier: 3407>: None,
    <PropertyIdentifier: 722>: None,
    <PropertyIdentifier: 3542>: None,
    <PropertyIdentifier: 3543>: None,
    <PropertyIdentifier: 3422>: None,
    <PropertyIdentifier: 725>: None,
    <PropertyIdentifier: 3164>: None,
    <PropertyIdentifier: update-interval>: 400,
    <PropertyIdentifier: 3544>: None,
    <PropertyIdentifier: 2179>: None,
    <PropertyIdentifier: 1294>: None,
    <PropertyIdentifier: 1293>: None,
    <PropertyIdentifier: 956>: None,
    <PropertyIdentifier: 1296>: None,
    <PropertyIdentifier: 1295>: None,
    <PropertyIdentifier: 2180>: None,
    <PropertyIdentifier: 1433>: None,
    <PropertyIdentifier: 518>: None,
    <PropertyIdentifier: 519>: None,
    <PropertyIdentifier: 3645>: None,
    <PropertyIdentifier: present-value>: -62.288089752197266,
    <PropertyIdentifier: out-of-service>: 0,
    <PropertyIdentifier: reliability>: <Reliability: no-sensor>,
    <PropertyIdentifier: status-flags>: <StatusFlags: fault>,
    <PropertyIdentifier: min-pres-value>: -46.0,
    <PropertyIdentifier: max-pres-value>: 121.0,
    <PropertyIdentifier: 913>: None,
    <PropertyIdentifier: units>: <EngineeringUnits: degrees-celsius>,
    <PropertyIdentifier: 661>: None,
    <PropertyIdentifier: cov-increment>: 0.15000000596046448,
    <PropertyIdentifier: resolution>: 0.10000000149011612,
    <PropertyIdentifier: device-type>: 'UI IN1',
    <PropertyIdentifier: event-state>: <EventState: normal>,
    <PropertyIdentifier: 3930>: None,
    <PropertyIdentifier: 3807>: None,
    <PropertyIdentifier: 569>: None,
    <PropertyIdentifier: event-detection-enable>: 0,
    <PropertyIdentifier: event-enable>: <EventTransitionBits: to-offnormal;to-fault;to-normal>,
    <PropertyIdentifier: limit-enable>: <LimitEnable: low-limit-enable;high-limit-enable>,
    <PropertyIdentifier: high-limit>: 70.0,
    <PropertyIdentifier: low-limit>: 65.0,
    <PropertyIdentifier: deadband>: 0.0,
    <PropertyIdentifier: time-delay>: 0,
    <PropertyIdentifier: notification-class>: 4194303,
    <PropertyIdentifier: notify-type>: <NotifyType: alarm>,
    <PropertyIdentifier: acked-transitions>: <EventTransitionBits: to-offnormal;to-fault;to-normal>,
    <PropertyIdentifier: event-time-stamps>: [
        <bacpypes3.basetypes.TimeStamp object at 0x00000277023C3B60>,
        <bacpypes3.basetypes.TimeStamp object at 0x00000276FE326090>,
        <bacpypes3.basetypes.TimeStamp object at 0x00000277021E4770>
    ],
    <PropertyIdentifier: event-message-texts>: ['', '', ''],
    <PropertyIdentifier: event-message-texts-config>: ['', '', ''],
    <PropertyIdentifier: 536>: None,
    <PropertyIdentifier: 32581>: None,
    <PropertyIdentifier: 32623>: None,
    <PropertyIdentifier: 4304>: None,
    <PropertyIdentifier: 4305>: None,
    <PropertyIdentifier: 4306>: None,
    <PropertyIdentifier: 32527>: None,
    <PropertyIdentifier: 2390>: None,
    <PropertyIdentifier: object-name>: 'DA-T',
    <PropertyIdentifier: description>: 'Discharge Air Temperature',
    <PropertyIdentifier: 512>: None,
    <PropertyIdentifier: 673>: None,
    <PropertyIdentifier: 2197>: None,
    <PropertyIdentifier: 908>: None,
    <PropertyIdentifier: 1006>: None,
    <PropertyIdentifier: object-type>: <ObjectType: analog-input>,
    <PropertyIdentifier: object-identifier>: (<ObjectType: analog-input>, 287831)
}
>>> from BAC0.core.proprietary_objects import jci_5
>>> await cgm['DA-T'].update_bacnet_properties()
Traceback (most recent call last):
  File "D:\0Programmes\Github\BAC0\BAC0\core\devices\Points.py", line 223, in update_bacnet_properties
    res = await self.properties.device.properties.network.readMultiple(
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\0Programmes\Github\BAC0\BAC0\core\io\Read.py", line 260, in readMultiple
    response = await _app.read_property_multiple(address, parameter_list)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\ctremblay\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\bacpypes3\service\object.py", line 623, in read_property_multiple
    property_value = read_result.propertyValue.cast_out(datatype)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\ctremblay\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\bacpypes3\constructeddata.py", line 1751, in cast_out
    result = cls.decode(tag_list)
             ^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\ctremblay\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\bacpypes3\primitivedata.py", line 1013, in decode
    raise InvalidTag(f"unsigned application tag expected, got {tag.tag_number}")
bacpypes3.errors.InvalidTag: unsigned application tag expected, got 0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.1520.0_x64__qbz5n2kfra8p0\Lib\concurrent\futures\_base.py", line 456, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.1520.0_x64__qbz5n2kfra8p0\Lib\concurrent\futures\_base.py", line 401, in __get_result
    raise self._exception
  File "<console>", line 1, in <module>
  File "D:\0Programmes\Github\BAC0\BAC0\core\devices\Points.py", line 239, in update_bacnet_properties
    raise Exception(f"Problem reading : {self.properties.name} | {e}")
Exception: Problem reading : DA-T | unsigned application tag expected, got 0
>>>
ChristianTremblay commented 3 months ago

It is related to this proprietary property which is not declared in the device.... and we really get NULL

        }[4]
            .... 1... = Tag Class: Context Specific Tag
            0100 .... = Context Tag Number: 4
            .... .111 = Named Tag: Closing Tag (7)
        Property Identifier: (3645) Vendor Proprietary Value (3645)
            Context Tag: 2, Length/Value/Type: 2
                .... 1... = Tag Class: Context Specific Tag
                0010 .... = Context Tag Number: 2
                Length Value Type: 2
            Property Identifier: Unknown (3645)
        {[4]
            .... 1... = Tag Class: Context Specific Tag
            0100 .... = Context Tag Number: 4
            .... .110 = Named Tag: Opening Tag (6)
        (3645) Vendor Proprietary Value: NULL
            Application Tag: Null, Length/Value/Type: 0
                .... 0... = Tag Class: Application Tag
                0000 .... = Application Tag Number: Null (0)
                Length Value Type: 0
        }[4]
ChristianTremblay commented 3 months ago

As this makes reading "all" fails... maybe BACpypes could just return None if tag is 0 ? Or the proprietary properties definition could support 2 types ? (in this case...unsigned OR null(?))

Thanks for your help with that

ChristianTremblay commented 3 months ago

The implementation works for me. Closing this