ChristianTremblay / BAC0

BAC0 - Library depending on BACpypes3 (Python 3) to build automation script for BACnet applications
GNU Lesser General Public License v3.0
174 stars 99 forks source link

How to write calendar entries to dateList? #258

Closed mosterme1003 closed 3 years ago

mosterme1003 commented 3 years ago

Hey! I would like to add entries to a calendar's property dateList. Now I have a list with multiple bacpypes.basetypes.CalendarEntry. How can I write it to dateList? I tried to figure out how you did similar things in the Schedule.py but I don't really get how this whole request building and sending processes work, especially when I'm sending a list or an array. Can you help me? Is there an easy way for me to write dates to a calendar?

mosterme1003 commented 3 years ago

I think I got it to work for me. Is this a clean way to do it?

from bacpypes.core import deferred
from bacpypes.pdu import Address
from BAC0.core.io.Read import find_reason
from BAC0.core.io.IOExceptions import NoResponseFromController, WritePropertyException
from bacpypes.apdu import WritePropertyRequest, SimpleAckPDU
from bacpypes.iocb import IOCB
from bacpypes.constructeddata import ArrayOf, Any
from bacpypes.basetypes import CalendarEntry
import BAC0

class Calendar:
    DateList = ArrayOf(CalendarEntry)

    def build_write_request(self, deviceIp, calendarInstance, dateList):
        request = WritePropertyRequest(
            objectIdentifier=("calendar", calendarInstance),
            propertyIdentifier="dateList"
        )
        request.pduDestination = Address(deviceIp)

        _value = Any()
        _value.cast_in(dateList)
        request.propertyValue = _value

        return request

    def write_value(self, bacnet, request, vendor_id=0, timeout=10):
        try:
            iocb = IOCB(request)
            iocb.set_timeout(timeout)
            # pass to the BACnet stack
            deferred(bacnet.this_application.request_io, iocb)

            iocb.wait()  # Wait for BACnet response

            if iocb.ioResponse:  # successful response
                apdu = iocb.ioResponse

                if not isinstance(apdu, SimpleAckPDU):  # expect an ACK
                    print("Not an ack, see debug for more infos.")
                    return

            if iocb.ioError:  # unsuccessful: error/reject/abort
                apdu = iocb.ioError
                reason = find_reason(apdu)
                raise NoResponseFromController(
                    "APDU Abort Reason : {}".format(reason))

        except WritePropertyException as error:
            # construction error
            print(("exception: {!r}".format(error)))

    def write_calendar(self, localIp, deviceIp, calendarInstance, dateList):
        bacnet = BAC0.connect(ip=localIp)
        request = self.build_write_request(deviceIp=deviceIp, calendarInstance=calendarInstance, dateList=dateList)
        self.write_value(bacnet, request)

if __name__ == '__main__':
    dt1 = (121, 2, 19, 255)
    dt2 = (121, 2, 20, 255)
    listEntries = [CalendarEntry(date=dt1), CalendarEntry(date=dt2)]
    dateList = Calendar.DateList(listEntries)

    Cal = Calendar()
    Cal.write_calendar(localIp="192.168.178.2", deviceIp="192.168.178.3", calendarInstance=10104, dateList=dateList)
ChristianTremblay commented 3 years ago

Now that the basic mechanism works, I'd like to think about the way BAC0 should represent the calendar, preferably with something like a dict, like schedules. So it's easy to read or write.

I'm curious also to see if we could use the calendar python package(https://github.com/python/cpython/blob/3.9/Lib/calendar.py)

Would be cool to have access to a calendar view...

mosterme1003 commented 3 years ago

I didn't look into calendar.py yet but I thought about a way to represent the calendar object's datelist as a dict. I also wrote the write_calendar_dateList() and read_calendar_dateList() functions that could be implemented in BAC0. Currently supported are (non-)recurring dates and date ranges but not yet the weeknday option of the calendar entry. What do you think?

from bacpypes.core import deferred
from bacpypes.pdu import Address
from BAC0.core.io.Read import find_reason
from BAC0.core.io.IOExceptions import NoResponseFromController, WritePropertyException
from bacpypes.apdu import WritePropertyRequest, SimpleAckPDU
from bacpypes.iocb import IOCB
from bacpypes.constructeddata import ArrayOf, Any
from bacpypes.basetypes import CalendarEntry, DateRange

import BAC0
import datetime

class Calendar:
    """
    Everything you need to write dates and date ranges to a calendar object.
    """

    DateList = ArrayOf(CalendarEntry)
    datelist_example = {
        "dates": [
            {
                "date": "2021/3/14",
                "recurring": False,
            },
            {
                "date": "2021/3/10",
                "recurring": True,
            },
        ],
        "dateRanges": [
            {
                "startDate": "2021/3/16",
                "endDate": "2021/3/21",
            },
            {
                "startDate": "2021/3/5",
                "endDate": "2021/3/7",
            },
        ]
    }

    def build_write_request(self, deviceIp, calendar_instance, dateList):
        request = WritePropertyRequest(
            objectIdentifier=("calendar", calendar_instance),
            propertyIdentifier="dateList"
        )
        request.pduDestination = Address(deviceIp)

        _value = Any()
        _value.cast_in(dateList)
        request.propertyValue = _value

        return request

    def write_value(self, bacnet, request, vendor_id=0, timeout=10):
        try:
            iocb = IOCB(request)
            iocb.set_timeout(timeout)
            # pass to the BACnet stack
            deferred(bacnet.this_application.request_io, iocb)

            iocb.wait()  # Wait for BACnet response

            if iocb.ioResponse:  # successful response
                apdu = iocb.ioResponse

                if not isinstance(apdu, SimpleAckPDU):  # expect an ACK
                    print("Not an ack, see debug for more infos.")
                    return

            if iocb.ioError:  # unsuccessful: error/reject/abort
                apdu = iocb.ioError
                reason = find_reason(apdu)
                raise NoResponseFromController(
                    "APDU Abort Reason : {}".format(reason))

        except WritePropertyException as error:
            # construction error
            print(("exception: {!r}".format(error)))

    def write_calendar_dateList(self, bacnet, deviceIp, calendar_instance, dateList_dict):
        """
        This function will create a dateList from the given dict and write it to
        the given calendar object.
        """

        try:
            dateList = self.encode_dateList(dateList_dict)
            request = self.build_write_request(deviceIp=deviceIp, calendar_instance=calendar_instance, dateList=dateList)
            self.write_value(bacnet, request)
        except Exception as error:
            print(("exception: {!r}".format(error)))

    def read_calendar_dateList(self, bacnet, deviceIp, calendar_instance):
        """
        This function will read the dateList property of given calendar object and
        pass it to decode_dateList() to convert it into a human readable dict.
        """

        try:
            dateList_object = bacnet.read("{} calendar {} dateList".format(deviceIp, calendar_instance))
            dateList_dict = self.decode_dateList(dateList_object)
        except Exception as error:
            print(("exception: {!r}".format(error)))
            return {}

        return dateList_dict

    def decode_dateList(self, dateList_object):
        dateList_dict = {"dates": [], "dateRanges": []}
        for entry in dateList_object:
            entry_dict = {}
            if entry.date:
                if entry.date[3] == 255:
                    recurring = True
                else:
                    recurring = False
                entry_dict["date"] = "{}/{}/{}".format(entry.date[0] + 1900, entry.date[1], entry.date[2])
                entry_dict["recurring"] = recurring
                dateList_dict["dates"].append(entry_dict)
            elif entry.dateRange:
                entry_dict["startDate"] = "{}/{}/{}".format(entry.dateRange.startDate[0] + 1900,
                                                            entry.dateRange.startDate[1],
                                                            entry.dateRange.startDate[2])
                entry_dict["endDate"] = "{}/{}/{}".format(entry.dateRange.endDate[0] + 1900,
                                                          entry.dateRange.endDate[1],
                                                          entry.dateRange.endDate[2])
                dateList_dict["dateRanges"].append(entry_dict)

        return dateList_dict

    def encode_dateList(self, dateList_dict):
        """
        From a structured dict (see dateList_example), create a DateList
        an ArrayOf(CalendarEntry)
        """

        entries = []
        if "dates" in dateList_dict.keys():
            for date_entry in dateList_dict["dates"]:
                year, month, day = (int(x) for x in date_entry["date"].split('/'))
                if date_entry["recurring"]:
                    weekday = 255
                else:
                    weekday = datetime.date(year, month, day).weekday() + 1
                    if weekday > 7: weekday = 1
                _date = (year - 1900, month, day, weekday)
                entries.append(CalendarEntry(date=_date))

        if "dateRanges" in dateList_dict.keys():
            for date_range_entry in dateList_dict["dateRanges"]:
                year, month, day = (int(x) for x in date_range_entry["startDate"].split('/'))
                weekday = datetime.date(year, month, day).weekday() + 1
                if weekday > 7: weekday = 1
                start_date = (year - 1900, month, day, weekday)

                year, month, day = (int(x) for x in date_range_entry["endDate"].split('/'))
                weekday = datetime.date(year, month, day).weekday() + 1
                if weekday > 7: weekday = 1
                end_date = (year - 1900, month, day, weekday)

                date_range = DateRange(startDate=start_date, endDate=end_date)
                entries.append(CalendarEntry(dateRange=date_range))

        dateList = self.DateList(entries)
        return dateList
github-actions[bot] commented 3 years ago

This issue had no activity for a long period of time. If this issue is still required, please update the status or else, it will be closed. Please note that an issue can be reopened if required.

ChristianTremblay commented 3 years ago

I've been on other things lately. Sorry. I'll find time to look at this.

ChristianTremblay commented 3 years ago

I'm actually adding this almost as-is Thanks

github-actions[bot] commented 3 years ago

This issue had no activity for a long period of time. If this issue is still required, please update the status or else, it will be closed. Please note that an issue can be reopened if required.