JoelBender / bacpypes

BACpypes provides a BACnet application layer and network layer written in Python for daemons, scripting, and graphical interfaces.
MIT License
299 stars 129 forks source link

Write Multiple Property #194

Closed spadilam closed 6 years ago

spadilam commented 6 years ago

Team we are trying to write writemutliple service by referring to the writeproperty and readmultipleproperty, but no luck. Below is the code used to achieve write multiple.


import sys

from bacpypes.debugging import bacpypes_debugging, ModuleLogger
from bacpypes.consolelogging import ConfigArgumentParser
from bacpypes.consolecmd import ConsoleCmd

from bacpypes.core import run, enable_sleeping
from bacpypes.iocb import IOCB
from bacpypes.core import deferred

from bacpypes.pdu import Address
from bacpypes.object import get_object_class, get_datatype

from bacpypes.apdu import WritePropertyMultipleRequest, PropertyReference, \
    WriteAccessSpecification, ReadPropertyMultipleACK

from bacpypes.apdu import SimpleAckPDU, \
    ReadPropertyRequest, ReadPropertyACK, WritePropertyRequest
from bacpypes.primitivedata import Null, Atomic, Boolean, Unsigned, Integer, \
    Real, Double, OctetString, CharacterString, BitString, Date, Time
from bacpypes.constructeddata import Array, Any, AnyAtomic

from bacpypes.app import BIPSimpleApplication
from bacpypes.local.device import LocalDeviceObject
from bacpypes.basetypes import PropertyIdentifier

import configparser
from threading import Thread

from bacpypes.core import run as startBacnetIPApp
from bacpypes.core import stop as stopBacnetIPApp

class WriteMultiple:
    def __init__(self, fileini):
        self.fileini = configparser.ConfigParser()
        self.fileini.read(fileini)
        self.this_device = LocalDeviceObject(
            objectName=self.fileini.get('BACpypes', 'objectName'),
            objectIdentifier=int(self.fileini.get('BACpypes', 'objectidentifier')),
            maxApduLengthAccepted=int(self.fileini.get('BACpypes', 'maxapdulengthaccepted')),
            segmentationSupported=self.fileini.get('BACpypes', 'segmentationsupported'),
            vendorIdentifier=int(self.fileini.get('BACpypes', 'vendoridentifier')),)```

        self.this_application = BIPSimpleApplication(self.this_device, self.fileini.get('BACpypes', 'address'))

        self.t = Thread(target=startBacnetIPApp, kwargs={
            'sigterm': None, 'sigusr1': None}, daemon=True)
        self.t.start()
        print('BAC0 started')

    def do_write(self, args):
        """write <addr> <type> <inst> <prop> <value> [ <indx> ] [ <priority> ]"""
        args = args.split()

        try:
            i = 0
            addr = args[i]
            i += 1

            write_access_spec_list = []
            write_values_list = []
            indx = None
            while i < len(args):
                obj_type = args[i]
                i += 1
                if obj_type.isdigit():
                    obj_type = int(obj_type)
                elif not get_object_class(obj_type):
                    raise ValueError("unknown object type")

                obj_inst = int(args[i])
                i += 1

                prop_reference_list = []
                j = i
                while j < len(args):
                    prop_id = args[j]
                    if prop_id not in PropertyIdentifier.enumerations:
                        break

                    if prop_id in ('all', 'required', 'optional'):
                        pass
                    else:
                        datatype = get_datatype(obj_type, prop_id)
                        if not datatype:
                            raise ValueError("invalid property for object type")

                    #build a property reference
                    prop_reference = PropertyReference(
                        propertyIdentifier=prop_id
                        )

                    # check for an array index
                    if (j < len(args)) and args[j].isdigit():
                        prop_reference.propertyArrayIndex = int(args[i])
                        j += 2
                    else:
                        j += 2
                    # add it to the list
                    prop_reference_list.append(prop_reference)

                # check for at least one property
                if not prop_reference_list:
                    raise ValueError("provide at least one property")
                while i < len(args):
                    i += 1
                    value = args[i]

                    # get the datatype
                    datatype = get_datatype(obj_type, args[i-1])
                    i += 1

                    # change atomic values into something encodeable, null is a special case
                    if value == 'null':
                        value = Null()
                    elif issubclass(datatype, AnyAtomic):
                        dtype, dvalue = value.split(':')

                        datatype = {
                            'b': Boolean,
                            'u': lambda x: Unsigned(int(x)),
                            'i': lambda x: Integer(int(x)),
                            'r': lambda x: Real(float(x)),
                            'd': lambda x: Double(float(x)),
                            'o': OctetString,
                            'c': CharacterString,
                            'bs': BitString,
                            'date': Date,
                            'time': Time,
                        }[dtype]

                        value = datatype(dvalue)

                    elif issubclass(datatype, Atomic):
                        if datatype is Integer:
                            value = int(value)
                        elif datatype is Real:
                            value = float(value)
                        elif datatype is Unsigned:
                            value = int(value)
                        else:
                            value = str(value)
                        value = datatype(value)
                    # elif issubclass(datatype, Array) and (indx is not None):
                    #     if indx == 0:
                    #         value = Integer(value)
                    #     elif issubclass(datatype.subtype, Atomic):
                    #         value = datatype.subtype(value)
                    #     elif not isinstance(value, datatype.subtype):
                    #         raise TypeError("invalid result datatype, expecting %s" % (datatype.subtype.__name__,))
                    elif not isinstance(value, datatype):
                        raise TypeError("invalid result datatype, expecting %s" % (datatype.__name__,))

                    # build a read access specification
                    write_access_spec = WriteAccessSpecification(
                        objectIdentifier=(obj_type, obj_inst),
                        listOfProperties=prop_reference_list,
                        )

                    # add it to the list
                    write_access_spec_list.append(write_access_spec)

                    # add values to list
                    write_values_list.append(value)

            # check for at least one
            if not write_access_spec_list:
                raise RuntimeError("at least one read access specification required")

            # build the request
            request = WritePropertyMultipleRequest(
                listOfWriteAccessSpecs=write_access_spec_list,
                )
            request.pduDestination = Address(addr)

            # save the value
            request.propertyValue = Array()
            try:
                #for element in write_values_list:
                request.propertyValue.cast_in(write_values_list)
            except Exception as error:
                print("WriteProperty cast error: %r", error)

            # make an IOCB
            iocb = IOCB(request)
            deferred(self.this_application.request_io, iocb)

            # give it to the application
            self.this_application.request_io(iocb)

            # wait for it to complete
            iocb.wait()

            # do something for success
            if iocb.ioResponse:
                apdu = iocb.ioResponse

                # should be an ack
                if not isinstance(apdu, SimpleAckPDU):
                    return

                return 'ack'

            # do something for error/reject/abort
            if iocb.ioError:
                print(str(iocb.ioError) + '\n')

        except Exception as error:
            print("exception: %r", error)

    def disconnect(self):
        stopBacnetIPApp()
        self.t.join()
JoelBender commented 6 years ago

1) The listOfProperties parameter of a WriteAccessSpecification is a sequence of PropertyValue, not a PropertyReference.

2) After you parse the value, it needs to go into the PropertyValue like prop_value.value = value, it's not a parameter of the request.

3) It looks like you are missing the priority.

It might make it simpler to understand if you parse the parameters in the same order as they are in the request, i.e., property identifier followed by the optional array index, then the value, then optionally the priority. Once you are happy with the way parsing a PropertyValue is working, you can nest that in a loop for more than one of them, then parse the object identifier off the front and build a WriteAccessSpecification, then loop around that for multiple objects.

spadilam commented 6 years ago

Thanks for the info ... Your suggestions helped us find the below code.

''' def do_writemultiple(self, args):

write [ ] [ ]

    args = args.split()

    try:
        i = 0
        addr = args[i]
        i += 1

        write_access_spec_list = []
        write_values_list = []

        while i < len(args):
            obj_type = args[i]
            i += 1
            if obj_type.isdigit():
                obj_type = int(obj_type)
            elif not get_object_class(obj_type):
                raise ValueError("unknown object type")

            obj_inst = int(args[i])
            i += 1

            prop_value_list = []

            while i < len(args):
                prop_id = args[i]
                if prop_id not in PropertyIdentifier.enumerations:
                    break

                if prop_id in ('all', 'required', 'optional'):
                    pass
                else:
                    datatype = get_datatype(obj_type, prop_id)
                    if not datatype:
                        raise ValueError("invalid property for object type")

                i += 1
                # get the property value from args
                value = args[i]

                # get the datatype
                datatype = get_datatype(obj_type, args[i-1])
                # increment for the next property
                i += 1

                # change atomic values into something encodeable, null is a special case
                if value == 'null':
                    value = Null()
                elif issubclass(datatype, AnyAtomic):
                    dtype, dvalue = value.split(':')

                    datatype = {
                        'b': Boolean,
                        'u': lambda x: Unsigned(int(x)),
                        'i': lambda x: Integer(int(x)),
                        'r': lambda x: Real(float(x)),
                        'd': lambda x: Double(float(x)),
                        'o': OctetString,
                        'c': CharacterString,
                        'bs': BitString,
                        'date': Date,
                        'time': Time,
                    }[dtype]

                    value = datatype(dvalue)

                elif issubclass(datatype, Atomic):
                    if datatype is Integer:
                        value = int(value)
                    elif datatype is Real:
                        value = float(value)
                    elif datatype is Unsigned:
                        value = int(value)

                    value = datatype(value)
                    value = Any(value)

                elif not isinstance(value, datatype):
                    raise TypeError("invalid result datatype, expecting %s" % (datatype.__name__,))

                # build a property reference
                prop_value = PropertyValue(propertyIdentifier=prop_id, value=value, priority=1)

                # add it to the list
                prop_value_list.append(prop_value)

                # check for an array index
                if (i < len(args)) and args[i].isdigit():
                    prop_value.propertyArrayIndex = int(args[i])

                # check for at least one property
                if not prop_value_list:
                    raise ValueError("provide at least one property")

                # build a read access specification
                write_access_spec = WriteAccessSpecification(
                    objectIdentifier=(obj_type, obj_inst),
                    listOfProperties=prop_value_list,
                    )

                # add it to the list
                write_access_spec_list.append(write_access_spec)

                # add values to list
                write_values_list.append(value)

        # check for at least one
        if not write_access_spec_list:
            raise RuntimeError("at least one read access specification required")

        # build the request
        request = WritePropertyMultipleRequest(
            listOfWriteAccessSpecs=write_access_spec_list,
            )
        request.pduDestination = Address(addr)

        # save the value
        request.propertyValue = Any()

        # make an IOCB
        iocb = IOCB(request)
        deferred(self.this_application.request_io, iocb)

        # give it to the application
        self.this_application.request_io(iocb)

        # wait for it to complete
        iocb.wait()

        # do something for success
        if iocb.ioResponse:
            apdu = iocb.ioResponse

            # should be an ack
            if not isinstance(apdu, SimpleAckPDU):
                return

            return 'ack'

        # do something for error/reject/abort
        if iocb.ioError:
            print(str(iocb.ioError) + '\n')

    except Exception as error:
        print("exception: %r", error)

'''

shashankgowdasd commented 1 year ago

Hi @spadilam @JoelBender , I am currently working on the Writepropertymultiple and i have taken the above as a reference to implement it but i am getting an Attribute Error : object has no attribute 'split' in args = args.split()

So please suggest on these

JoelBender commented 1 year ago

Print out the contents of args like print(type(args), args) and see what is being passed into the function.

shashankgowdasd commented 1 year ago

Hi , @JoelBender I got this when I print(type(args), args) <class 'bacpypes.apdu.WritePropertyMultipleRequest'> <bacpypes.apdu.WritePropertyMultipleRequest(16,1) instance at 0xb5ce5b08>

From the client side, I am sending the two Objects like ['analogOutput 1 presentValue 33','binaryOutput 1 presentValue inactive']

please tell me to access the values of the particular property (ex:presentValue) of request where I can access objectIdentifier and propertyIdentifier but not the propertyValue.So please help me to find out access the value

JoelBender commented 1 year ago

Good start, now check out the class definition here and you'll see that it is composed of a single element, a list of WriteAccessSpecification, which contains an objectIdentifier. So try:

for i, elem in enumerate(apdu.listOfWriteAccessSpecs):
    print(i, ":", elem.objectIdentifier)

and you'll see that piece of the puzzle. Keep digging!

shashankgowdasd commented 1 year ago

I am able to get the details of objectIdentifier,propertyIdentifier,propertyArrayIndex and priority but not the propertyValue,So kindly suggest me how to access the propertyValue from the WriteMultiplePropertyRequest

FYI

def do_WritePropertyMultipleRequest(self, apdu):
      if apdu.serviceChoice == 16:
                    print("in writemultiple")
                    if isinstance(apdu, WritePropertyMultipleRequest):
                        print("inside if")
                        print(f"APDU = {apdu}")
                        write_access_specs = apdu.listOfWriteAccessSpecs
                        for write_access_spec in write_access_specs:
                            object_id = write_access_spec.objectIdentifier
                            obj = self.get_object_id(object_id)
                            obj_type,obj_inst = object_id         
                            for prop_reference in write_access_spec.listOfProperties:
                                obj_count_wm += 1
                                property_id = prop_reference.propertyIdentifier
                                property_value = prop_reference.value
                                property_arrIndex = prop_reference.propertyArrayIndex
                                property_priority = prop_reference.priority 

For propertyValue when I tried to print I got an Value like <bacpypes.constructeddata.Any object at 0xb52c3598> and I couldn't able to decipher it

JoelBender commented 1 year ago

Any is a challenge because it can be anything! It could be a simple thing, like an integer or boolean, or a list of things, or "context encoded" blob of bits that you need to know more. Inside the Any object is a tagList which is a list of "tags" used to encode primitive data standardized in BACnet, or "open" and "close" tags that act like the beginning and end of a structure, or just some blob with a context number.

If you look in the standard you'll see that most of the property values of objects are the same type, like the Object_Name is always a character string. But the Present_Value property depends on the object type, so when you get the value back it can be anything, and the ReadProperty service is designed to read anything. The tag list has a debug_contents() method that can help.

shashankgowdasd commented 1 year ago

Thanks @JoelBender for the suggestion

Apologies for the late reply

Regarding the WriteMultipleProperty, I am able to get the "propertyValue" and able to send the response back to the client..