diff --git a/BAC0/core/app/ScriptApplication.py b/BAC0/core/app/ScriptApplication.py index 0ca32d24..f8e88457 100644 --- a/BAC0/core/app/ScriptApplication.py +++ b/BAC0/core/app/ScriptApplication.py @@ -2,49 +2,46 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -ScriptApplication +# +''' +ScriptApplication ================= -Built around a simple BIPSimpleApplication this module deals with requests -created by a child app. It will prepare the requests and give them back to the -stack. - -This module will also listen for responses to requests (indication or confirmation). -Response will be added to a Queue, we'll wait for the response to be processed -by the caller, then resume. +A basic BACnet application (bacpypes BIPSimpleApplication) for interacting with +the bacpypes BACnet stack. It enables the base-level BACnet functionality +(a.k.a. device discovery) - meaning it can send & receive WhoIs & IAm messages. -This object will be added to script objects and will be runned as thread +Additional functionality is enabled by inheriting this application, and then +extending it with more functions. [See BAC0.scripts for more examples of this.] -See BAC0.scripts for more details. - -""" +''' +#--- standard Python modules --- +from collections import defaultdict +import logging +#--- 3rd party modules --- from bacpypes.debugging import bacpypes_debugging from bacpypes.app import BIPSimpleApplication from bacpypes.pdu import Address -from collections import defaultdict -import logging - +#--- this application's modules --- from ..functions.debug import log_debug + +#------------------------------------------------------------------------------ + @bacpypes_debugging class ScriptApplication(BIPSimpleApplication): """ - This class defines the bacnet application that process requests - """ - - def __init__(self, *args): - """ - Creation of the application. Adding properties to basic B/IP App. + Defines a basic BACnet/IP application to process BACnet requests. - :param *args: local object device, local IP address + :param *args: local object device, local IP address See BAC0.scripts.BasicScript for more details. - """ + + """ + def __init__(self, *args): logging.getLogger("comtypes").setLevel(logging.INFO) self.localAddress = None @@ -62,9 +59,8 @@ def __init__(self, *args): else: self.local_unicast_tuple = ('', 47808) self.local_broadcast_tuple = ('255.255.255.255', 47808) - - #log_debug(ScriptApplication, "__init__ %r" % args) + def do_WhoIsRequest(self, apdu): """Respond to a Who-Is request.""" if self._debug: ScriptApplication._debug("do_WhoIsRequest %r", apdu) @@ -72,8 +68,7 @@ def do_WhoIsRequest(self, apdu): # build a key from the source and parameters key = (str(apdu.pduSource), apdu.deviceInstanceRangeLowLimit, - apdu.deviceInstanceRangeHighLimit, - ) + apdu.deviceInstanceRangeHighLimit ) # count the times this has been received self.who_is_counter[key] += 1 @@ -81,16 +76,13 @@ def do_WhoIsRequest(self, apdu): # continue with the default implementation BIPSimpleApplication.do_WhoIsRequest(self, apdu) + def do_IAmRequest(self, apdu): """Given an I-Am request, cache it.""" if self._debug: ScriptApplication._debug("do_IAmRequest %r", apdu) # build a key from the source, just use the instance number - key = (str(apdu.pduSource), - apdu.iAmDeviceIdentifier[1], - ) - - # count the times this has been received + key = (str(apdu.pduSource), apdu.iAmDeviceIdentifier[1] ) self.i_am_counter[key] += 1 # continue with the default implementation diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py index 29763cf3..0a39375a 100644 --- a/BAC0/core/devices/Device.py +++ b/BAC0/core/devices/Device.py @@ -1,36 +1,27 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # -# Copyright (C) 2015 by Christian Tremblay, P.Eng -# +# Copyright (C) 2015-2017 by Christian Tremblay, P.Eng # Licensed under LGPLv3, see file LICENSE in this source tree. +# +''' +Device.py - describe a BACnet Device -""" -How to describe a bacnet device -""" -from bacpypes.basetypes import ServicesSupported - -from .Points import NumericPoint, BooleanPoint, EnumPoint, OfflinePoint -from ..io.IOExceptions import NoResponseFromController, ReadPropertyMultipleException, SegmentationNotSupported -from ...bokeh.BokehRenderer import BokehPlot -from ...sql.sql import SQLMixin -from ...tasks.DoOnce import DoOnce -#from .states.DeviceDisconnected import DeviceDisconnected -from .mixins.read_mixin import ReadPropertyMultiple, ReadProperty - +''' +#--- standard Python modules --- from collections import namedtuple -import pandas as pd from datetime import datetime +import os.path +from abc import ABCMeta # abstract base classes + +#--- 3rd party modules --- import sqlite3 + import pandas as pd from pandas.lib import Timestamp from pandas.io import sql -from abc import ABCMeta - -import os.path - try: from xlwings import Workbook, Sheet, Range, Chart _XLWINGS = True @@ -38,20 +29,20 @@ print('xlwings not installed. If using Windows or OSX, install to get more features.') _XLWINGS = False -# Credit : Raymond Hettinger +#--- this application's modules --- +from bacpypes.basetypes import ServicesSupported + +from .Points import NumericPoint, BooleanPoint, EnumPoint, OfflinePoint +from ..io.IOExceptions import NoResponseFromController, ReadPropertyMultipleException, SegmentationNotSupported +from ...bokeh.BokehRenderer import BokehPlot +from ...sql.sql import SQLMixin +from ...tasks.DoOnce import DoOnce +#from .states.DeviceDisconnected import DeviceDisconnected +from .mixins.read_mixin import ReadPropertyMultiple, ReadProperty -#def fix_docs(cls): -# for name, func in vars(cls).items(): -# if not func.__doc__: -# #print(func, 'needs doc') -# for parent in cls.__bases__: -# parfunc = getattr(parent, name) -# if parfunc and getattr(parfunc, '__doc__', None): -# #func.__doc__ = parfunc.__doc__ -# break -# return cls +#------------------------------------------------------------------------------ class DeviceProperties(object): """ @@ -79,25 +70,22 @@ def __repr__(self): def asdict(self): return self.__dict__ + class Device(SQLMixin): """ - Bacnet device - This class represents a controller. When defined, it allows - the use of read, write, sim, release functions to communicate - with the device on the network + Represent a BACnet device. Once defined, it allows use of read, write, sim, release + functions to communicate with the device on the network. + + :param addr: address of the device (ex. '2:5') + :param device_id: bacnet device ID (boid) + :param network: defined by BAC0.connect() + :param poll: (int) if > 0, will poll every points each x seconds. + + :type address: (str) + :type device_id: int + :type network: BAC0.scripts.ReadWriteScript.ReadWriteScript """ - def __init__(self, address, device_id, network, *, poll=10, from_backup = None, segmentation_supported = True): - """ - Initialization require address, device id and bacnetApp (the script itself) - :param addr: address of the device (ex. '2:5') - :param device_id: bacnet device ID (boid) - :param network: defined by BAC0.connect() - :param poll: (int) if > 0, will poll every points each x seconds. - :type address: (str) - :type device_id: int - :type network: BAC0.scripts.ReadWriteScript.ReadWriteScript - """ self.properties = DeviceProperties() self.properties.address = address @@ -122,15 +110,12 @@ def __init__(self, address, device_id, network, *, poll=10, from_backup = None, self._polling_task.task = None self._polling_task.running = False - self._notes = namedtuple('_notes', - ['timestamp', 'notes']) - + self._notes = namedtuple('_notes',['timestamp', 'notes']) self._notes.timestamp = [] self._notes.notes = [] self._notes.notes.append("Controller initialized") self._notes.timestamp.append(datetime.now()) - if from_backup: filename = from_backup db_name = filename.split('.')[0] @@ -142,19 +127,21 @@ def __init__(self, address, device_id, network, *, poll=10, from_backup = None, else: self.new_state(DeviceDisconnected) + def new_state(self, newstate): """ - Base of the state machine mechanism - Used to make transitions between device states - Take care of calling the state init function + Base of the state machine mechanism. + Used to make transitions between device states. + Take care to call the state init function. """ - print('Changing device state to %s' % newstate) + print('Changing device state to {}'.format(newstate)) self.__class__ = newstate self._init_state() + def _init_state(self): """ - This function allow running some code upon state modification + Execute additional code upon state modification """ raise NotImplementedError() @@ -165,47 +152,53 @@ def connect(self): """ raise NotImplementedError() + def disconnect(self): raise NotImplementedError() + def initialize_device_from_db(self): raise NotImplementedError() + @property def notes(self): """ - Notes allow the user to add text notes to the device. + Allow the addition of text notes to the device. Notes are stored as timeseries (same than points) + :returns: pd.Series """ - notes_table = pd.Series(self._notes.notes, - index=self._notes.timestamp) + notes_table = pd.Series(self._notes.notes, index=self._notes.timestamp) return notes_table + @notes.setter def notes(self, note): """ Setter for notes + :param note: (str) """ self._notes.timestamp.append(datetime.now()) self._notes.notes.append(note) + def df(self, list_of_points, force_read=True): """ - df is a way to build a pandas DataFrame from a list of points - DataFrame can be used to present data or analysis + Build a pandas DataFrame from a list of points. DataFrames are used to present and analyze data. :param list_of_points: a list of point names as str :returns: pd.DataFrame """ raise NotImplementedError() + def chart(self, list_of_points, *, title='Live Trending', show_notes=True): """ - chart offers a way to draw a chart from a list of points. - It allows to pass args to the pandas plot() functions - refer to the pandas and matplotlib doc for details. + Draw a chart from a list of points. Refer to the pandas and matplotlib doc for details on + the plot() function and the args they accept. + :param list_of_points: a list of point name as str :param plot_args: arg for plot function :returns: plot() @@ -214,6 +207,7 @@ def chart(self, list_of_points, *, title='Live Trending', show_notes=True): update_data = False else: update_data = True + if self.properties.network.bokehserver: lst = [] for point in list_of_points: @@ -222,6 +216,7 @@ def chart(self, list_of_points, *, title='Live Trending', show_notes=True): lst.append(point) else: print('Wrong name, removing %s from list' % point) + try: self.properties.serving_chart[title] = BokehPlot( self, lst, title=title, show_notes=show_notes, update_data=update_data) @@ -230,10 +225,12 @@ def chart(self, list_of_points, *, title='Live Trending', show_notes=True): else: print("No bokeh server running, can't display chart") + @property def simulated_points(self): """ iterate over simulated points + :returns: points if simulated (out_of_service == True) :rtype: BAC0.core.devices.Points.Point """ @@ -241,18 +238,19 @@ def simulated_points(self): if each.properties.simulated: yield each + def _buildPointList(self): """ - Read all points of the device and creates a dataframe (Pandas) to store - the list and allow quick access. - This list will be used to access variables based on point name + Read all points from a device into a (Pandas) dataframe (Pandas). Items are + accessible by point name. """ raise NotImplementedError() + def __getitem__(self, point_name): """ - Get a point based on its name - If a list is passed, will return a dataframe + Get a point from its name. + If a list is passed - a dataframe is returned. :param point_name: (str) name of the point or list of point_names :type point_name: str @@ -260,16 +258,19 @@ def __getitem__(self, point_name): """ raise NotImplementedError() + def __iter__(self): """ When iterating a device, iterate points of it. """ raise NotImplementedError() + def __contains__(self, value): "When using in..." raise NotImplementedError() + @property def points_name(self): """ @@ -277,15 +278,18 @@ def points_name(self): """ raise NotImplementedError() + def to_excel(self): """ Using xlwings, make a dataframe of all histories and save it """ raise NotImplementedError() + def __setitem__(self, point_name, value): """ Write, sim or ovr value + :param point_name: Name of the point to set :param value: value to write to the point :type point_name: str @@ -293,22 +297,25 @@ def __setitem__(self, point_name, value): """ raise NotImplementedError() + def __len__(self): """ Will return number of points available """ raise NotImplementedError() + def _parseArgs(self, arg): """ - Given a string, will interpret the last word as the value, everything else - will be considered the point name + Given a string, interpret the last word as the value, everything else is + considered to be the point name. """ args = arg.split() pointName = ' '.join(args[:-1]) value = args[-1] return (pointName, value) + @property def analog_units(self): raise NotImplementedError() @@ -329,41 +336,23 @@ def multi_states(self): def binary_states(self): raise NotImplementedError() -# def _discoverPoints(self): -# """ -# This function allows the discovery of all bacnet points in a device -# -# :returns: (deviceName, pss, objList, df) -# :rtype: tuple -# -# *deviceName* : name of the device -# *pss* : protocole service supported -# *objList* : list of bacnet object (ex. analogInput, 1) -# *df* : is a dataFrame containing pointType, pointAddress, pointName, description -# presentValue and units -# -# If pandas can't be found, df will be a simple array -# -# """ -# raise NotImplementedError() def _findPoint(self, name, force_read=True): """ Helper that retrieve point based on its name. :param name: (str) name of the point - :param force_read: (bool) read value of the point each time the func - is called. + :param force_read: (bool) read value of the point each time the function is called. :returns: Point object - :rtype: BAC0.core.devices.Point.Point (NumericPoint, EnumPoint or - BooleanPoint) - + :rtype: BAC0.core.devices.Point.Point (NumericPoint, EnumPoint or BooleanPoint) """ raise NotImplementedError() + def do(self, func): DoOnce(func).start() + def __repr__(self): return '%s / Undefined' % self.properties.name @@ -371,22 +360,25 @@ def __repr__(self): #@fix_docs class DeviceConnected(Device): """ - If the device is found on the bacnet network, its state will be connected. - Once connected, every command will use the bacnet connection. + Find a device on the BACnet network. Set its state to 'connected'. + Once connected, all subsequent commands use this BACnet connection. """ def _init_state(self): self._buildPointList() + def disconnect(self): print('Wait while stopping polling') self.poll(command='stop') self.new_state(DeviceFromDB) + def connect(self, *, db = None): """ - A connected device can be switched to a DBmode where the device will - not use the bacnet network but the data saved previously. + A connected device can be switched to 'database mode' where the device will + not use the BACnet network but instead obtain its contents from a previously + stored database. """ if db: self.poll(command = 'stop') @@ -395,6 +387,7 @@ def connect(self, *, db = None): else: print('Already connected, provide db arg if you want to connect to db') + def df(self, list_of_points, force_read=True): """ When connected, calling DF should force a reading on the network. @@ -410,26 +403,28 @@ def df(self, list_of_points, force_read=True): return pd.DataFrame(dict(zip(list_of_points, his))) + def _buildPointList(self): """ - Initial work upon connection to build the device point list - and properties. + Upon connection to build the device point list and properties. """ try: self.properties.pss.value = self.properties.network.read( - '%s device %s protocolServicesSupported' % - (self.properties.address, self.properties.device_id)) + '{} device {} protocolServicesSupported'.format(self.properties.address, self.properties.device_id)) + except NoResponseFromController as error: - print('Controller not found, aborting. (%s)' % error) + print('Controller not found, aborting. ({})'.format(error)) return ('Not Found', '', [], []) + except SegmentationNotSupported as error: print('Segmentation not supported') self.segmentation_supported = False self.new_state(DeviceDisconnected) + self.properties.name = self.properties.network.read( - '%s device %s objectName' % - (self.properties.address, self.properties.device_id)) - print('Found %s... building points list' % self.properties.name) + '{} device {} objectName'.format(self.properties.address, self.properties.device_id)) + + print('Device {}:[{}] found... building points list'.format(self.properties.device_id,self.properties.name)) try: self.properties.objects_list, self.points = self._discoverPoints() if self.properties.pollDelay > 0: @@ -442,13 +437,11 @@ def _buildPointList(self): def __getitem__(self, point_name): """ - Allow the usage of - device['point_name'] or - device[lst_of_points] + Allows the syntax: device['point_name'] or device[list_of_points] If calling a list, last value will be used (won't read on the network) for performance reasons. - If calling a simple point, point will be read via bacnet. + If calling a simple point, point will be read via BACnet. """ try: if isinstance(point_name, list): @@ -458,27 +451,29 @@ def __getitem__(self, point_name): except ValueError as ve: print('%s' % ve) + def __iter__(self): for each in self.points: yield each + def __contains__(self, value): """ - Allow + Allows the syntax: if "point_name" in device: - use case """ return value in self.points_name + @property def points_name(self): for each in self.points: yield each.properties.name + def to_excel(self): """ - This is a beta function allowing the creation of an Excel document - based on the device point histories. + Create an Excel spreadsheet from the device's point histories. """ his = {} for name in list(self.points_name): @@ -498,34 +493,36 @@ def to_excel(self): else: df.to_csv() + def __setitem__(self, point_name, value): """ - Allow the usage of + Allows the syntax: device['point_name'] = value - use case """ try: self._findPoint(point_name)._set(value) except ValueError as ve: print('%s' % ve) + def __len__(self): """ Length of a device = number of points """ return len(self.points) + def _parseArgs(self, arg): args = arg.split() pointName = ' '.join(args[:-1]) value = args[-1] return (pointName, value) + @property def analog_units(self): """ - A shortcut to retrieve all analog points units - Used by Bokeh feature + Shortcut to retrieve all analog points units [Used by Bokeh trending feature] """ au = [] us = [] @@ -535,18 +532,21 @@ def analog_units(self): us.append(each.properties.units_state) return dict(zip(au, us)) + @property def temperatures(self): for each in self.analog_units.items(): if "deg" in each[1]: yield each + @property def percent(self): for each in self.analog_units.items(): if "percent" in each[1]: yield each + @property def multi_states(self): ms = [] @@ -557,16 +557,17 @@ def multi_states(self): us.append(each.properties.units_state) return dict(zip(ms, us)) + @property def binary_states(self): bs = [] us = [] + for each in self.points: if isinstance(each, BooleanPoint): bs.append(each.properties.name) us.append(each.properties.units_state) return dict(zip(bs, us)) - def _findPoint(self, name, force_read=True): @@ -580,74 +581,74 @@ def _findPoint(self, name, force_read=True): return point raise ValueError("%s doesn't exist in controller" % name) + def __repr__(self): return '%s / Connected' % self.properties.name + +#------------------------------------------------------------------------------ + class RPDeviceConnected(DeviceConnected, ReadProperty): """ - A device will be in that state if it's connected but doesn't support - read property multiple + [Device state] If device is connected but doesn't support ReadPropertyMultiple - In that state, BAC0 will not poll all points every 10 seconds by default. - Would be too much trafic. Polling must be done as needed using poll function + BAC0 will not poll such points automatically (since it would cause excessive network traffic). + Instead manual polling must be used as needed via the poll() function. """ def __str__(self): - return 'connected using read property' + return 'connected [for ReadProperty]' + class RPMDeviceConnected(DeviceConnected, ReadPropertyMultiple): """ - A device will be in that state if it's connected and support - read property multiple + [Device state] If device is connected and supports ReadPropertyMultiple """ def __str__(self): - return 'connected using read property multiple' + return 'connected [for ReadPropertyMultiple]' + #@fix_docs class DeviceDisconnected(Device): """ - Initial state of a device. Disconnected from bacnet. + [Device state] Initial state of a device. Disconnected from BACnet. """ def _init_state(self): self.connect() + def connect(self, *, db = None): """ - Will try to connect, if unable, will connect to a database if one's - available (so the user can play with previous data) + Attempt to connect to device. If unable, attempt to connect to a controller database + (so the user can use previously saved data). """ if db: self.properties.db_name = db try: - ojbect_list = self.properties.network.read( - '%s device %s objectList' % - (self.properties.address, self.properties.device_id)) - if ojbect_list: + object_list = self.properties.network.read('{} device {} objectList'.format( + self.properties.address, self.properties.device_id)) + + if object_list: if self.segmentation_supported: self.new_state(RPMDeviceConnected) else: self.new_state(RPDeviceConnected) - + except SegmentationNotSupported: self.segmentation_supported = False - print('Segmentation not supported.... will slow down our requests') + print('Segmentation not supported.... expect slow responses.') self.new_state(RPDeviceConnected) - except NoResponseFromController: + except (NoResponseFromController, AttributeError): if self.properties.db_name: self.new_state(DeviceFromDB) else: - print('Provide dbname to connect to device offline') + print('Offline: provide database name to load stored data.') print("Ex. controller.connect(db = 'backup')") - except AttributeError: - if self.properties.db_name: - self.new_state(DeviceFromDB) - else: - print('Provide dbname to connect to device offline') - print("Ex. controller.connect(db = 'backup')") def df(self, list_of_points, force_read=True): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + @property def simulated_points(self): @@ -655,50 +656,63 @@ def simulated_points(self): if each.properties.simulated: yield each + def _buildPointList(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') # This should be a "read" function and rpm defined in state rpm def read_multiple(self, points_list, *, points_per_request=25, discover_request=(None, 6)): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def poll(self, command='start', *, delay=10): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def __getitem__(self, point_name): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def __iter__(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def __contains__(self, value): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + @property def points_name(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def to_excel(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def __setitem__(self, point_name, value): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def __len__(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + @property def analog_units(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + @property def temperatures(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + @property def percent(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + @property def multi_states(self): @@ -706,26 +720,27 @@ def multi_states(self): @property def binary_states(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def _discoverPoints(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def _findPoint(self, name, force_read=True): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def __repr__(self): return '%s / Disconnected' % self.properties.name +#------------------------------------------------------------------------------ + #@fix_docs class DeviceFromDB(DeviceConnected): """ - This state is used when replaying previous data. - Every call to - device['point_name'] - will result on last valid value. - - Histories for each point are available + [Device state] Where requests for a point's present value returns the last + valid value from the point's history. """ def _init_state(self): try: @@ -733,31 +748,36 @@ def _init_state(self): except ValueError: self.new_state(DeviceDisconnected) + def connect(self, *, network = None, from_backup = None): """ - In a DBState, a device can be reconnected to bacnet using : + In DBState, a device can be reconnected to BACnet using: device.connect(bacnet) (bacnet = BAC0.connect()) """ if network and from_backup: raise WrongParameter('Please provide network OR from_backup') + elif network: self.properties.network = network try: - ojbect_list = self.properties.network.read( - '%s device %s objectList' % - (self.properties.address, self.properties.device_id)) - if ojbect_list: + object_list = self.properties.network.read('{} device {} objectList'.format( + self.properties.address, self.properties.device_id)) + + if object_list: if self.segmentation_supported: self.new_state(RPMDeviceConnected) else: self.new_state(RPDeviceConnected) self.db.close() + except NoResponseFromController: print('Unable to connect, keeping DB mode active') + elif from_backup: self.properties.db_name = from_backup.split('.')[0] self._init_state() + def initialize_device_from_db(self): print('Initializing DB') # Save important properties for reuse @@ -791,35 +811,46 @@ def initialize_device_from_db(self): self.properties.multistates = self._props['multistates'] print('Device restored from db') + @property def simulated_points(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def _buildPointList(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + # This should be a "read" function and rpm defined in state rpm def read_multiple(self, points_list, *, points_per_request=25, discover_request=(None, 6)): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def poll(self, command='start', *, delay=10): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def __contains__(self, value): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def to_excel(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def __setitem__(self, point_name, value): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def _discoverPoints(self): - raise DeviceNotConnected('Must connect to bacnet or database') + raise DeviceNotConnected('Must connect to BACnet or database') + def __repr__(self): return '%s / Disconnected' % self.properties.name +#------------------------------------------------------------------------------ + class DeviceLoad(DeviceFromDB): def __init__(self,filename = None): if filename: @@ -827,8 +858,11 @@ def __init__(self,filename = None): else: raise Exception('Please provide backup file as argument') + # Some exceptions class DeviceNotConnected(Exception): pass + class WrongParameter(Exception): - pass \ No newline at end of file + pass + diff --git a/BAC0/core/devices/Points.py b/BAC0/core/devices/Points.py index 13f2d239..4fc28282 100644 --- a/BAC0/core/devices/Points.py +++ b/BAC0/core/devices/Points.py @@ -2,29 +2,33 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -Definition of points so operations will be easier on read result -""" +# +''' +Points.py - Definition of points so operations on Read results are more convenient. +''' -import pandas as pd +#--- standard Python modules --- from datetime import datetime from collections import namedtuple import time -from ...tasks.Poll import SimplePoll as Poll -from ...tasks.Match import Match -from ..io.IOExceptions import NoResponseFromController, WriteAccessDenied, UnknownPropertyError - +#--- 3rd party modules --- import sqlite3 import pandas as pd from pandas.io import sql from pandas.lib import Timestamp +#--- this application's modules --- +from ...tasks.Poll import SimplePoll as Poll +from ...tasks.Match import Match, Match_Value +from ..io.IOExceptions import NoResponseFromController, WriteAccessDenied, UnknownPropertyError + +#------------------------------------------------------------------------------ + class PointProperties(object): """ - This serves as a container for properties. + A container for point properties. """ def __init__(self): self.device = None @@ -34,36 +38,32 @@ def __init__(self): self.description = None self.units_state = None self.simulated = (False, None) - self.overridden = (False, None) - + self.overridden = (False, None) + + def __repr__(self): return '%s' % self.asdict + @property def asdict(self): return self.__dict__ +#------------------------------------------------------------------------------ + class Point(): """ - This represents a point inside a device. This base class will be - used to build NumericPoint, BooleanPoint and EnumPoints - - Each point implement a history feature. Each time the point is read, - the value with timstampe is added to a - history table. It's then possible to look what happened since the - creation of the point. + Represents a device BACnet point. Used to NumericPoint, BooleanPoint and EnumPoints. + Each point implements a history feature. Each time the point is read, its value (with timestamp) + is added to a history table. Histories capture the changes to point values over time. """ def __init__(self, device=None, - pointType=None, - pointAddress=None, - pointName=None, - description=None, - presentValue=None, - units_state=None): - self._history = namedtuple('_history', - ['timestamp', 'value']) + pointType=None, pointAddress=None, pointName=None, + description=None, presentValue=None, units_state=None): + + self._history = namedtuple('_history',['timestamp', 'value']) self.properties = PointProperties() self._polling_task = namedtuple('_polling_task', ['task', 'running']) @@ -83,31 +83,33 @@ def __init__(self, device=None, self.properties.name = pointName self.properties.type = pointType self.properties.address = pointAddress + self.properties.description = description self.properties.units_state = units_state self.properties.simulated = (False, 0) self.properties.overridden = (False, 0) + @property def value(self): """ Retrieve value of the point """ try: - res = self.properties.device.properties.network.read( - '%s %s %s presentValue' % - (self.properties.device.properties.address, self.properties.type, str( - self.properties.address))) + res = self.properties.device.properties.network.read('{} {} {} presentValue'.format( + self.properties.device.properties.address, self.properties.type, str(self.properties.address))) self._trend(res) except Exception: raise Exception('Problem reading : %s' % self.properties.name) return res - + + def _trend(self, res): self._history.timestamp.append(datetime.now()) self._history.value.append(res) + @property def units(self): """ @@ -115,6 +117,7 @@ def units(self): """ raise Exception('Must be overridden') + @property def lastValue(self): """ @@ -122,21 +125,24 @@ def lastValue(self): """ return self._history.value[-1] + @property def history(self): """ returns : (pd.Series) containing timestamp and value of all readings """ - his_table = pd.Series(self._history.value, - index=self._history.timestamp) + his_table = pd.Series(self._history.value, index=self._history.timestamp) return his_table + def chart(self, *args): """ Simple shortcut to plot function """ args = args.split() - return self.history.replace(['inactive', 'active'], [0, 1]).plot('%s, title = %s / %s' % (args, self.properties.name, self.properties.description)) + return self.history.replace(['inactive', 'active'], [0, 1]).plot( + '{}, title = {} / {}'.format(args, self.properties.name, self.properties.description)) + def __getitem__(self, key): """ @@ -152,6 +158,7 @@ def __getitem__(self, key): except AttributeError: raise ValueError('Wrong property') + def write(self, value, *, prop='presentValue', priority=''): """ Write to present value of a point @@ -180,51 +187,57 @@ def write(self, value, *, prop='presentValue', priority=''): # Read after the write so history gets updated. self.value + def default(self, value): self.write(value, prop='relinquishDefault') + def sim(self, value, *, force=False): """ - Simulate a value - Will write to out_of_service property (true) - Will then write the presentValue so the controller will use that value - The point name will be added to the list of simulated points - (self.simPoints) + Simulate a value. Sets the Out_Of_Service property- to disconnect the point from the + controller's control. Then writes to the Present_Value. + The point name is added to the list of simulated points (self.simPoints) :param value: (float) value to simulate - """ if self.properties.simulated[0] \ and self.properties.simulated[1] == value \ and force == False: pass else: - self.properties.device.properties.network.sim( - '%s %s %s presentValue %s' % - (self.properties.device.properties.address, self.properties.type, str( - self.properties.address), str(value))) + self.properties.device.properties.network.sim('{} {} {} presentValue {}'.format( + self.properties.device.properties.address, self.properties.type, str(self.properties.address), str(value))) self.properties.simulated = (True, value) + + def out_of_service(self): + """ + Sets the Out_Of_Service property [to True]. + """ + self.properties.device.properties.network.out_of_service('{} {} {}'.format( + self.properties.device.properties.address, self.properties.type, str(self.properties.address))) + self.properties.simulated = (True, None) + + def release(self): """ - Release points - Will write to out_of_service property (false) - The controller will take control back of the presentValue + Clears the Out_Of_Service property [to False] - so the controller regains control of the point. """ - self.properties.device.properties.network.release( - '%s %s %s' % - (self.properties.device.properties.address, self.properties.type, str( - self.properties.address))) + self.properties.device.properties.network.release('{} {} {}'.format( + self.properties.device.properties.address, self.properties.type, str(self.properties.address))) self.properties.simulated = (False, None) + def ovr(self, value): self.write(value, priority=8) self.properties.overridden = (True, value) + def auto(self): self.write('null', priority=8) self.properties.overridden = (False, 0) + def _setitem(self, value): """ Called by _set, will trigger right function depending on @@ -253,42 +266,47 @@ def _setitem(self, value): else: self.sim(value) + def _set(self, value): """ - This function will check for datatype to write - and call _setitem() - Those functions allow __setitem__ to work from device - device['point'] = value + Allows the syntax: + device['point'] = value """ raise Exception('Must be overridden') + def poll(self, command='start', *, delay=10): """ - Enable polling of a variable. Will be read every x seconds (delay=x sec) - Can be stopped by using point.poll('stop') or .poll(0) or .poll(False) + Poll a point every x seconds (delay=x sec) + Stopped by using point.poll('stop') or .poll(0) or .poll(False) or by setting a delay = 0 """ if str(command).lower() == 'stop' \ or command == False \ or command == 0 \ or delay == 0: + if isinstance(self._polling_task.task, Poll): self._polling_task.task.stop() self._polling_task.task = None self._polling_task.running = False + elif self._polling_task.task is None: self._polling_task.task = Poll(self, delay=delay) self._polling_task.task.start() self._polling_task.running = True + elif self._polling_task.running: self._polling_task.task.stop() self._polling_task.running = False self._polling_task.task = Poll(self, delay=delay) self._polling_task.task.start() self._polling_task.running = True + else: raise RuntimeError('Stop polling before redefining it') + def match(self, point, *, delay=5): """ This allow functions like : @@ -297,23 +315,55 @@ def match(self, point, *, delay=5): A fan status for example will follow the command... """ if self._match_task.task is None: - self._match_task.task = Match( - command=point, status=self, delay=delay) + self._match_task.task = Match(command=point, status=self, delay=delay) + self._match_task.task.start() + self._match_task.running = True + + elif self._match_task.running and delay > 0: + self._match_task.task.stop() + self._match_task.running = False + time.sleep(1) + + self._match_task.task = Match(command=point, status=self, delay=delay) + self._match_task.task.start() + self._match_task.running = True + + elif self._match_task.running and delay == 0: + self._match_task.task.stop() + self._match_task.running = False + + else: + raise RuntimeError('Stop task before redefining it') + + + def match_value(self, value, *, delay=5): + """ + This allow functions like : + device['point'].match('value') + + A sensor will follow a calculation... + """ + if self._match_task.task is None: + self._match_task.task = Match_Value(value=value, point=self, delay=delay) self._match_task.task.start() self._match_task.running = True + elif self._match_task.running and delay > 0: self._match_task.task.stop() self._match_task.running = False time.sleep(1) - self._match_task.task = Match( - command=point, status=self, delay=delay) + + self._match_task.task = Match_Value(value=value, point=self, delay=delay) self._match_task.task.start() self._match_task.running = True + elif self._match_task.running and delay == 0: self._match_task.task.stop() self._match_task.running = False + else: raise RuntimeError('Stop task before redefining it') + def __len__(self): """ @@ -322,30 +372,26 @@ def __len__(self): return len(self.history) +#------------------------------------------------------------------------------ + class NumericPoint(Point): """ - Representation of a Numeric information + Representation of a Numeric value """ def __init__(self, device=None, - pointType=None, - pointAddress=None, - pointName=None, - description=None, - presentValue=None, - units_state=None): + pointType=None, pointAddress=None, pointName=None, + description=None, presentValue=None, units_state=None): + Point.__init__(self, device=device, - pointType=pointType, - pointAddress=pointAddress, - pointName=pointName, - description=description, - presentValue=presentValue, - units_state=units_state) + pointType=pointType, pointAddress=pointAddress, pointName=pointName, + description=description, presentValue=presentValue, units_state=units_state) @property def units(self): return self.properties.units_state + def _set(self, value): if str(value).lower() == 'auto': self._setitem(value) @@ -359,8 +405,10 @@ def _set(self, value): except: raise ValueError('Value must be numeric') + def __repr__(self): return '%s : %.2f %s' % (self.properties.name, self.history.dropna().iloc[-1], self.properties.units_state) + def __add__(self,other): return self.value + other @@ -388,38 +436,32 @@ def __gt__(self,other): def __ge__(self,other): return self.value >= other + +#------------------------------------------------------------------------------ class BooleanPoint(Point): """ - Representation of a Boolean Information + Representation of a Boolean value """ def __init__(self, device=None, - pointType=None, - pointAddress=None, - pointName=None, - description=None, - presentValue=None, - units_state=None): + pointType=None, pointAddress=None, pointName=None, + description=None, presentValue=None, units_state=None): + Point.__init__(self, device=device, - pointType=pointType, - pointAddress=pointAddress, - pointName=pointName, - description=description, - presentValue=presentValue, - units_state=units_state) + pointType=pointType, pointAddress=pointAddress, pointName=pointName, + description=description, presentValue=presentValue, units_state=units_state) @property def value(self): """ - Read the value on bacnet using network read (bacpypes) + Read the value from BACnet network """ try: - res = self.properties.device.properties.network.read( - '%s %s %s presentValue' % - (self.properties.device.properties.address, self.properties.type, str( - self.properties.address))) + res = self.properties.device.properties.network.read('{} {} {} presentValue'.format( + self.properties.device.properties.address, self.properties.type, str(self.properties.address))) self._trend(res) + except Exception: raise Exception('Problem reading : %s' % self.properties.name) @@ -431,6 +473,7 @@ def value(self): self._boolKey = True return res + @property def boolValue(self): """ @@ -444,6 +487,7 @@ def boolValue(self): self._boolKey = False return self._boolKey + @property def units(self): """ @@ -451,6 +495,7 @@ def units(self): """ return None + def _set(self, value): if value == True: self._setitem('active') @@ -477,25 +522,20 @@ def __xor__(self,other): def __eq__(self,other): return self.boolValue == other +#------------------------------------------------------------------------------ + class EnumPoint(Point): """ - Representation of an Enumerated Information (multiState) + Representation of an Enumerated (multiState) value """ - def __init__(self, device=None, - pointType=None, - pointAddress=None, - pointName=None, - description=None, - presentValue=None, - units_state=None): + pointType=None, pointAddress=None, pointName=None, + description=None, presentValue=None, units_state=None): + Point.__init__(self, device=device, - pointType=pointType, - pointAddress=pointAddress, - pointName=pointName, - description=description, - presentValue=presentValue, - units_state=units_state) + pointType=pointType, pointAddress=pointAddress, pointName=pointName, + description=description, presentValue=presentValue, units_state=units_state) + @property def enumValue(self): @@ -504,13 +544,15 @@ def enumValue(self): """ return self.properties.units_state[int(self.history.dropna().iloc[-1]) - 1] + @property def units(self): """ - Enum point don't use units... we use states text instead + Enums have 'state text' instead of units. """ return None + def _set(self, value): if isinstance(value, int): self._setitem(value) @@ -522,12 +564,17 @@ def _set(self, value): raise ValueError( 'Value must be integer or correct enum state : %s' % self.properties.units_state) + def __repr__(self): # return '%s : %s' % (self.name, ) return '%s : %s' % (self.properties.name, self.enumValue) + def __eq__(self,other): return self.value == self.properties.units_state.index(other) + 1 + + +#------------------------------------------------------------------------------ class OfflinePoint(Point): """ @@ -539,9 +586,11 @@ def __init__(self, device, name): self.properties.device = device dev_name = self.properties.device.properties.db_name props = self.properties.device.read_point_prop(dev_name, name) + self.properties.name = props['name'] self.properties.type = props['type'] self.properties.address = props['address'] + self.properties.description = props['description'] self.properties.units_state = props['units_state'] self.properties.simulated = 'Offline' @@ -555,7 +604,8 @@ def __init__(self, device, name): self.new_state(BooleanPointOffline) else: raise TypeError('Unknown point type') - + + def new_state(self, newstate): self.__class__ = newstate @@ -577,6 +627,7 @@ def value(self): except IndexError: value = 65535 return value + def write(self, value, *, prop='presentValue', priority=''): raise OfflineException('Must be online to write') @@ -586,6 +637,7 @@ def sim(self, value, *, prop='presentValue', priority=''): def release(self, value, *, prop='presentValue', priority=''): raise OfflineException('Must be online to write') + @property def units(self): @@ -597,6 +649,7 @@ def _set(self, value): def __repr__(self): return '%s : %.2f %s' % (self.properties.name, self.value, self.properties.units_state) + class BooleanPointOffline(BooleanPoint): @property def history(self): @@ -612,6 +665,7 @@ def value(self): value = 'NaN' return value + def _set(self, value): raise OfflineException('Point must be online to write') @@ -624,6 +678,7 @@ def sim(self, value, *, prop='presentValue', priority=''): def release(self, value, *, prop='presentValue', priority=''): raise OfflineException('Must be online to write') + class EnumPointOffline(EnumPoint): @property def history(self): @@ -644,6 +699,7 @@ def value(self): value = 'NaN' return value + @property def enumValue(self): """ @@ -657,6 +713,7 @@ def enumValue(self): value = 'NaN' return value + def _set(self, value): raise OfflineException('Point must be online to write') @@ -669,6 +726,7 @@ def sim(self, value, *, prop='presentValue', priority=''): def release(self, value, *, prop='presentValue', priority=''): raise OfflineException('Must be online to write') + class OfflineException(Exception): pass \ No newline at end of file diff --git a/BAC0/core/devices/mixins/read_mixin.py b/BAC0/core/devices/mixins/read_mixin.py index 1533ed9f..dba98c4a 100644 --- a/BAC0/core/devices/mixins/read_mixin.py +++ b/BAC0/core/devices/mixins/read_mixin.py @@ -2,24 +2,37 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. +# +''' +read_mixin.py - Add ReadProperty and ReadPropertyMultiple to a device +''' +#--- standard Python modules --- + +#--- 3rd party modules --- +#--- this application's modules --- from ....tasks.Poll import DevicePoll from ...io.IOExceptions import ReadPropertyMultipleException, NoResponseFromController, SegmentationNotSupported from ..Points import NumericPoint, BooleanPoint, EnumPoint, OfflinePoint +#------------------------------------------------------------------------------ + +def retrieve_type(obj_list, point_type_key): + for point_type, point_address in obj_list: + if point_type_key in str(point_type): + yield (point_type, point_address) + class ReadPropertyMultiple(): """ - This is a Mixin that will handle ReadProperty and ReadPropertyMultiple - for a device + Handle ReadPropertyMultiple for a device """ + def _batches(self, request, points_per_request): """ - Generator that creates request batches. - Each batch will contain a maximum of - "points_per_request" points to read. + Generator for creating 'request batches'. Each batch contains a maximum of "points_per_request" + points to read. :params: request a list of point_name as a list :params: (int) points_per_request :returns: (iter) list of point_name of size <= points_per_request @@ -27,6 +40,7 @@ def _batches(self, request, points_per_request): for i in range(0, len(request), points_per_request): yield request[i:i + points_per_request] + def _rpm_request_by_name(self, point_list): """ :param point_list: a list of point @@ -38,185 +52,195 @@ def _rpm_request_by_name(self, point_list): str_list = [] point = self._findPoint(each, force_read=False) points.append(point) + str_list.append(' ' + point.properties.type) str_list.append(' ' + str(point.properties.address)) str_list.append(' presentValue') rpm_param = (''.join(str_list)) requests.append(rpm_param) + return (requests, points) + def read_multiple(self, points_list, *, points_per_request=25, discover_request=(None, 6)): """ - Functions to read points from a device using the read property - multiple request. - Using readProperty request can be very slow to read a lot of data. + Read points from a device using a ReadPropertyMultiple request. + [ReadProperty requests are very slow in comparison]. :param points_list: (list) a list of all point_name as str :param points_per_request: (int) number of points in the request - Using too many points will create big requests needing segmentation. - It's better to use just enough request so the message will not require - segmentation. + Requesting many points results big requests that need segmentation. Aim to request + just the 'right amount' so segmentation can be avoided. Determining the 'right amount' + is often trial-&-error. :Example: device.read_multiple(['point1', 'point2', 'point3'], points_per_request = 10) """ - #print('PSS : %s' % self.properties.pss['readPropertyMultiple']) if not self.properties.pss['readPropertyMultiple']: print('Read property Multiple Not supported') self.read_single(points_list,points_per_request=1, discover_request=discover_request) else: if not self.properties.segmentation_supported: points_per_request = 1 + if discover_request[0]: values = [] info_length = discover_request[1] big_request = discover_request[0] - # print(big_request) - for request in self._batches(big_request, - points_per_request): + + for request in self._batches(big_request, points_per_request): + try: - - request = ('%s %s' % - (self.properties.address, ''.join(request))) - + request = ('{} {}'.format(self.properties.address, ''.join(request))) + print('RPM_Request: ', request) val = self.properties.network.readMultiple(request) + + #print('val : ', val, len(val), type(val)) if val == None: self.properties.segmentation_supported = False raise SegmentationNotSupported + except KeyError as error: raise Exception('Unknown point name : %s' % error) except SegmentationNotSupported as error: self.properties.segmentation_supported = False self.read_multiple(points_list,points_per_request=1, discover_request=discover_request) - print('Seg not supported') - # Save each value to history of each point + print('Segmentation not supported') + else: for points_info in self._batches(val, info_length): values.append(points_info) return values + else: big_request = self._rpm_request_by_name(points_list) i = 0 - for request in self._batches(big_request[0], - points_per_request): + for request in self._batches(big_request[0], points_per_request): try: - request = ('%s %s' % - (self.properties.address, ''.join(request))) + request = ('{} {}'.format(self.properties.address, ''.join(request))) val = self.properties.network.readMultiple(request) + except SegmentationNotSupported as error: self.properties.segmentation_supported = False self.read_multiple(points_list,points_per_request=1, discover_request=discover_request) - # Save each value to history of each point + except KeyError as error: raise Exception('Unknown point name : %s' % error) - # Save each value to history of each point + else: points_values = zip(big_request[1][i:i + len(val)], val) i += len(val) for each in points_values: each[0]._trend(each[1]) - + + def read_single(self, points_list, *, points_per_request=1, discover_request=(None, 4)): if discover_request[0]: values = [] info_length = discover_request[1] big_request = discover_request[0] - # print(big_request) - for request in self._batches(big_request, - points_per_request): + + for request in self._batches(big_request, points_per_request): try: - - request = ('%s %s' % - (self.properties.address, ''.join(request))) - + request = ('{} {}'.format(self.properties.address, ''.join(request))) val = self.properties.network.read(request) + except KeyError as error: raise Exception('Unknown point name : %s' % error) + # Save each value to history of each point for points_info in self._batches(val, info_length): values.append(points_info) + return values + else: big_request = self._rpm_request_by_name(points_list) i = 0 - for request in self._batches(big_request[0], - points_per_request): + for request in self._batches(big_request[0], points_per_request): try: - request = ('%s %s' % - (self.properties.address, ''.join(request))) + request = ('{} {}'.format(self.properties.address, ''.join(request))) val = self.properties.network.read(request) points_values = zip(big_request[1][i:i + len(val)], val) + i += len(val) for each in points_values: each[0]._trend(each[1]) + except KeyError as error: raise Exception('Unknown point name : %s' % error) - # Save each value to history of each point + def _discoverPoints(self): try : objList = self.properties.network.read( - '%s device %s objectList' % - (self.properties.address, self.properties.device_id)) + '{} device {} objectList'.format(self.properties.address, self.properties.device_id)) + except SegmentationNotSupported: objList = [] number_of_objects = self.properties.network.read( - '%s device %s objectList' % - (self.properties.address, self.properties.device_id), arr_index = 0) + '{} device {} objectList'.format(self.properties.address, self.properties.device_id), arr_index = 0) + for i in range(1,number_of_objects+1): objList.append(self.properties.network.read( - '%s device %s objectList' % - (self.properties.address, self.properties.device_id), arr_index = i)) - + '{} device {} objectList'.format(self.properties.address, self.properties.device_id), arr_index = i)) points = [] - #print(objList) - def retrieve_type(obj_list, point_type_key): - """ - retrive analog values - """ - for point_type, point_address in obj_list: - if point_type_key in point_type: - yield (point_type, point_address) + # Numeric analog_request = [] list_of_analog = retrieve_type(objList, 'analog') for analog_points, address in list_of_analog: - analog_request.append('%s %s objectName presentValue units description ' % - (analog_points, address)) + analog_request.append('{} {} objectName presentValue units description '.format(analog_points, address)) + try: - analog_points_info = self.read_multiple( - '', discover_request=(analog_request, 4), points_per_request=5) + analog_points_info = self.read_multiple('', discover_request=(analog_request, 4), points_per_request=5) + print(analog_points_info) except SegmentationNotSupported: raise + i = 0 for each in retrieve_type(objList, 'analog'): point_type = str(each[0]) point_address = str(each[1]) point_infos = analog_points_info[i] + + if len(point_infos) == 4: + point_units_state = point_infos[2] + point_description = point_infos[3] + + elif len(point_infos) == 3: + #we probably get only objectName, presentValue and units + point_units_state = point_infos[2] + point_description = "" + + elif len(point_infos) == 2: + point_units_state = "" + point_description = "" + + else: + #raise ValueError('Not enough values returned', each, point_infos) + # SHOULD SWITCH TO SEGMENTATION_SUPPORTED = FALSE HERE + print('Cannot add {} / {} | {}'.format(point_type, point_address, len(point_infos))) + continue + i += 1 points.append( NumericPoint( - pointType=point_type, - pointAddress=point_address, - pointName=point_infos[0], - description=point_infos[3], - presentValue=float(point_infos[1]), - units_state=point_infos[2], + pointType=point_type, pointAddress=point_address, pointName=point_infos[0], + description=point_description, presentValue=float(point_infos[1]), units_state=point_units_state, device=self)) multistate_request = [] list_of_multistate = retrieve_type(objList, 'multi') for multistate_points, address in list_of_multistate: - multistate_request.append('%s %s objectName presentValue stateText description ' % - (multistate_points, address)) + multistate_request.append('{} {} objectName presentValue stateText description '.format(multistate_points, address)) - multistate_points_info = self.read_multiple( - '', discover_request=(multistate_request, 4), points_per_request=5) + multistate_points_info= self.read_multiple('', discover_request=(multistate_request, 4), points_per_request=5) i = 0 for each in retrieve_type(objList, 'multi'): @@ -224,64 +248,62 @@ def retrieve_type(obj_list, point_type_key): point_address = str(each[1]) point_infos = multistate_points_info[i] i += 1 + points.append( EnumPoint( - pointType=point_type, - pointAddress=point_address, - pointName=point_infos[0], - description=point_infos[3], - presentValue=point_infos[1], - units_state=point_infos[2], + pointType=point_type, pointAddress=point_address, pointName=point_infos[0], + description=point_infos[3], presentValue=point_infos[1], units_state=point_infos[2], device=self)) binary_request = [] list_of_binary = retrieve_type(objList, 'binary') for binary_points, address in list_of_binary: - binary_request.append('%s %s objectName presentValue inactiveText activeText description ' % - (binary_points, address)) + binary_request.append('{} {} objectName presentValue inactiveText activeText description '.format(binary_points, address)) - binary_points_info = self.read_multiple( - '', discover_request=(binary_request, 5), points_per_request=5) + binary_points_info= self.read_multiple('', discover_request=(binary_request, 5), points_per_request=5) i = 0 for each in retrieve_type(objList, 'binary'): point_type = str(each[0]) point_address = str(each[1]) point_infos = binary_points_info[i] + if len(point_infos) == 3: #we probably get only objectName, presentValue and description point_units_state = ('OFF', 'ON') point_description = point_infos[2] + elif len(point_infos) == 5: point_units_state = (point_infos[2], point_infos[3]) try: point_description = point_infos[4] except IndexError: point_description = "" + elif len(point_infos) == 2: point_units_state = ('OFF', 'ON') point_description = "" + else: #raise ValueError('Not enough values returned', each, point_infos) # SHOULD SWITCH TO SEGMENTATION_SUPPORTED = FALSE HERE - print('Cannot add %s / %s' % (point_type, point_address)) + print('Cannot add {} / {]'.format(point_type, point_address)) continue + i += 1 points.append( BooleanPoint( - pointType=point_type, - pointAddress=point_address, - pointName=point_infos[0], - description=point_description, - presentValue=point_infos[1], - units_state=point_units_state, + pointType=point_type, pointAddress=point_address, pointName=point_infos[0], + description=point_description, presentValue=point_infos[1], units_state=point_units_state, device=self)) + print('Ready!') return (objList, points) + def poll(self, command='start', *, delay=10): """ - Enable polling of a variable. Will be read every x seconds (delay=x sec) + Poll a point every x seconds (delay=x sec) Can be stopped by using point.poll('stop') or .poll(0) or .poll(False) or by setting a delay = 0 @@ -300,40 +322,46 @@ def poll(self, command='start', *, delay=10): or command == False \ or command == 0 \ or delay == 0: + if isinstance(self._polling_task.task, DevicePoll): self._polling_task.task.stop() while self._polling_task.task.is_alive(): pass + self._polling_task.task = None self._polling_task.running = False print('Polling stopped') + elif self._polling_task.task is None: self._polling_task.task = DevicePoll(self, delay=delay) self._polling_task.task.start() self._polling_task.running = True - print('Polling started, every values read each %s seconds' % delay) + print('Polling started, values read every {} seconds'.format(delay)) + elif self._polling_task.running: self._polling_task.task.stop() while self._polling_task.task.is_alive(): pass + self._polling_task.running = False self._polling_task.task = DevicePoll(self, delay=delay) self._polling_task.task.start() self._polling_task.running = True print('Polling started, every values read each %s seconds' % delay) + else: raise RuntimeError('Stop polling before redefining it') + class ReadProperty(): """ - This is a Mixin that will handle ReadProperty and ReadPropertyMultiple - for a device + Handle ReadProperty for a device """ + def _batches(self, request, points_per_request): """ - Generator that creates request batches. - Each batch will contain a maximum of - "points_per_request" points to read. + Generator for creating 'request batches'. Each batch contains a maximum of "points_per_request" + points to read. :params: request a list of point_name as a list :params: (int) points_per_request :returns: (iter) list of point_name of size <= points_per_request @@ -341,6 +369,7 @@ def _batches(self, request, points_per_request): for i in range(0, len(request), points_per_request): yield request[i:i + points_per_request] + def _rpm_request_by_name(self, point_list): """ :param point_list: a list of point @@ -352,17 +381,19 @@ def _rpm_request_by_name(self, point_list): str_list = [] point = self._findPoint(each, force_read=False) points.append(point) + str_list.append(' ' + point.properties.type) str_list.append(' ' + str(point.properties.address)) str_list.append(' presentValue') rpm_param = (''.join(str_list)) requests.append(rpm_param) + return (requests, points) + def read_multiple(self, points_list, *, points_per_request=1, discover_request=(None, 6)): """ - Functions to read points from a device using the read property - multiple request. + Functions to read points from a device using the ReadPropertyMultiple request. Using readProperty request can be very slow to read a lot of data. :param points_list: (list) a list of all point_name as str @@ -383,42 +414,35 @@ def read_multiple(self, points_list, *, points_per_request=1, discover_request=( else: self.read_single(points_list,points_per_request=1, discover_request=discover_request) + def read_single(self, request, *, points_per_request=1, discover_request=(None, 4)): try: - request = ('%s %s' % - (self.properties.address, ''.join(request))) + request = ('{} {}'.format(self.properties.address, ''.join(request))) return self.properties.network.read(request) + except KeyError as error: - raise Exception('Unknown point name : %s' % error) + raise Exception('Unknown point name: %s' % error) + except NoResponseFromController as error: return '' - # Save each value to history of each point def _discoverPoints(self): try : - objList = self.properties.network.read( - '%s device %s objectList' % - (self.properties.address, self.properties.device_id)) + objList = self.properties.network.read('{} device {} objectList'.format( + self.properties.address, self.properties.device_id)) + except SegmentationNotSupported: objList = [] number_of_objects = self.properties.network.read( - '%s device %s objectList' % - (self.properties.address, self.properties.device_id), arr_index = 0) + '{} device {} objectList'.format(self.properties.address, self.properties.device_id), arr_index = 0) + for i in range(1,number_of_objects+1): objList.append(self.properties.network.read( - '%s device %s objectList' % - (self.properties.address, self.properties.device_id), arr_index = i)) + '{} device {} objectList'.format(self.properties.address, self.properties.device_id), arr_index = i)) points = [] - def retrieve_type(obj_list, point_type_key): - """ - retrive analog values - """ - for point_type, point_address in obj_list: - if point_type_key in point_type: - yield (point_type, point_address) # Numeric for each in retrieve_type(objList, 'analog'): @@ -429,14 +453,12 @@ def retrieve_type(obj_list, point_type_key): NumericPoint( pointType=point_type, pointAddress=point_address, - pointName=self.read_single('%s %s objectName ' % - (point_type, point_address)), - description=self.read_single('%s %s description ' % - (point_type, point_address)), - presentValue=float(self.read_single('%s %s presentValue ' % - (point_type, point_address)),), - units_state=self.read_single('%s %s units ' % - (point_type, point_address)), + pointName=self.read_single('{} {} objectName '.format(point_type, point_address)), + description=self.read_single('{} {} description '.format(point_type, point_address)), + + presentValue=float( + self.read_single('{} {} presentValue '.format(point_type, point_address))), + units_state=self.read_single('{} {} units '.format(point_type, point_address)), device=self)) for each in retrieve_type(objList, 'multi'): @@ -447,14 +469,11 @@ def retrieve_type(obj_list, point_type_key): EnumPoint( pointType=point_type, pointAddress=point_address, - pointName=self.read_single('%s %s objectName ' % - (point_type, point_address)), - description=self.read_single('%s %s description ' % - (point_type, point_address)), - presentValue=(self.read_single('%s %s presentValue ' % - (point_type, point_address)),), - units_state=self.read_single('%s %s stateText ' % - (point_type, point_address)), + pointName=self.read_single('{} {} objectName '.format(point_type, point_address)), + description=self.read_single('{} {} description '.format(point_type, point_address)), + + presentValue=(self.read_single('{} {} presentValue '.format(point_type, point_address)),), + units_state=self.read_single('{} {} stateText '.format(point_type, point_address)), device=self)) for each in retrieve_type(objList, 'binary'): @@ -465,26 +484,23 @@ def retrieve_type(obj_list, point_type_key): BooleanPoint( pointType=point_type, pointAddress=point_address, - pointName=self.read_single('%s %s objectName ' % - (point_type, point_address)), - description=self.read_single('%s %s description ' % - (point_type, point_address)), - presentValue=(self.read_single('%s %s presentValue ' % - (point_type, point_address)),), + pointName=self.read_single('{} {} objectName '.format(point_type, point_address)), + description=self.read_single('{} {} description '.format(point_type, point_address)), + + presentValue=(self.read_single('{} {} presentValue '.format(point_type, point_address)),), units_state=( - (self.read_single('%s %s inactiveText ' % - (point_type, point_address))), - (self.read_single('%s %s activeText ' % - (point_type, point_address))) + (self.read_single('{} {} inactiveText '.format(point_type, point_address))), + (self.read_single('{} {} activeText '.format(point_type, point_address))) ), device=self)) print('Ready!') return (objList, points) + def poll(self, command='start', *, delay=60): """ - Enable polling of a variable. Will be read every x seconds (delay=x sec) + Poll a point every x seconds (delay=x sec) Can be stopped by using point.poll('stop') or .poll(0) or .poll(False) or by setting a delay = 0 diff --git a/BAC0/core/functions/GetIPAddr.py b/BAC0/core/functions/GetIPAddr.py index 85373f45..ce4e89e2 100644 --- a/BAC0/core/functions/GetIPAddr.py +++ b/BAC0/core/functions/GetIPAddr.py @@ -2,121 +2,61 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -Utility function to retrieve a functionnal IP and a correct broadcast IP -address. -Goal : not use 255.255.255.255 as a broadcast IP address as it is not -accepted by every devices (>3.8.38.1 bacnet.jar of Tridium Jace for example) - -""" -from bacpypes.pdu import Address - -from ..io.IOExceptions import NetworkInterfaceException +# +''' +GetIPAddr.py - Utility functions to retrieve local host's IP information. +''' -import socket +#--- standard Python modules --- import subprocess import ipaddress import sys +#--- 3rd party modules --- +from bacpypes.pdu import Address + +#--- this application's modules --- + +#------------------------------------------------------------------------------ class HostIP(): """ - Special class to identify host IP informations + Identify host's IP information """ - def __init__(self, ip=None, mask = None): - if ip: - ip = ip - else: - ip = self._findIPAddr() - if mask: - mask = mask + if 'win' in sys.platform: + proc = subprocess.Popen('ipconfig', stdout=subprocess.PIPE) + for l in proc.stdout: + line= str(l) + if 'Address' in line: + ip= line.split(':')[-1] + if 'Mask' in line: + mask= line.split(':')[-1] + + self.interface = ipaddress.IPv4Interface('{}/{}'.format(ip, mask)) else: - mask = self._findSubnetMask(ip) - self.interface = ipaddress.IPv4Interface("%s/%s" % (ip, mask)) + proc = subprocess.Popen('ifconfig', stdout=subprocess.PIPE) + for l in proc.stdout: + line= l.decode('utf-8') + if 'Bcast' in line: + _,ipaddr,bcast,mask= line.split() + _,ip= ipaddr.split(':') + _,mask= mask.split(':') + + self.interface = ipaddress.IPv4Interface('{}/{}'.format(ip, mask)) + break + self.interface = ipaddress.IPv4Interface('{}/{}'.format(ip, mask)) @property def ip_address(self): - """ - IP Address/subnet - """ - return ('%s/%s' % (self.interface.ip.compressed, - self.interface.exploded.split('/')[-1])) + return str(self.interface) # IP Address/subnet @property def address(self): - """ - IP Address using bacpypes Address format - """ - return (Address('%s/%s' % (self.interface.ip.compressed, - self.interface.exploded.split('/')[-1]))) - - def _findIPAddr(self): - """ - Retrieve the IP address connected to internet... used as - a default IP address when defining Script - - :returns: IP Adress as String - """ - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - s.connect(('google.com', 0)) - addr = s.getsockname()[0] - print('Using ip : {addr}'.format(addr=addr)) - s.close() - except socket.error: - raise NetworkInterfaceException( - 'Impossible to retrieve IP, please provide one manually') - return addr - - def _findSubnetMask(self, ip): - """ - Retrieve the broadcast IP address connected to internet... used as - a default IP address when defining Script - - :param ip: (str) optionnal IP address. If not provided, default to getIPAddr() - :param mask: (str) optionnal subnet mask. If not provided, will try to find one using ipconfig (Windows) or ifconfig (Linux or MAC) - - :returns: broadcast IP Adress as String - """ - ip = ip - - if 'win' in sys.platform: - try: - proc = subprocess.Popen('ipconfig', stdout=subprocess.PIPE) - while True: - line = proc.stdout.readline() - if ip.encode() in line: - break - mask = proc.stdout.readline().rstrip().split( - b':')[-1].replace(b' ', b'').decode() - except: - raise NetworkInterfaceException('Cannot read IP parameters from OS') - else: - """ - This procedure could use more direct way of obtaining the broadcast IP - as it is really simple in Unix - ifconfig gives Bcast directly for example - or use something like : - iface = "eth0" - socket.inet_ntoa(fcntl.ioctl(socket.socket(socket.AF_INET, socket.SOCK_DGRAM), 35099, struct.pack('256s', iface))[20:24]) - """ - try: - proc = subprocess.Popen('ifconfig', stdout=subprocess.PIPE) - while True: - line = proc.stdout.readline() - if ip.encode() in line: - break - mask = line.rstrip().split( - b':')[-1].replace(b' ', b'').decode() - except: - mask = '255.255.255.255' - - return mask + return (Address(str(self.interface))) # bacpypes format: ip if __name__ == '__main__': h = HostIP() - print(h.ip_address) + print('Localhost IP address= ',h.ip_address) diff --git a/BAC0/core/functions/PrintDebug.py b/BAC0/core/functions/PrintDebug.py index 8af9b399..b4842796 100644 --- a/BAC0/core/functions/PrintDebug.py +++ b/BAC0/core/functions/PrintDebug.py @@ -2,15 +2,14 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -Module : PrintDebug.py -Author : Christian Tremblay, ing. -Inspired a lot by the work of Joel Bender (joel@carrickbender.com) -Email : christian.tremblay@servisys.com -""" +# +''' +PrintDebug.py +Inspired by the work of Joel Bender (joel@carrickbender.com) +Email : christian.tremblay@servisys.com +''' def print_debug(msg, args): """ diff --git a/BAC0/core/functions/WhoisIAm.py b/BAC0/core/functions/WhoisIAm.py index 651f4f85..82089cac 100644 --- a/BAC0/core/functions/WhoisIAm.py +++ b/BAC0/core/functions/WhoisIAm.py @@ -2,19 +2,20 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -This module allows the creation of Whois and IAm requests by and app +# +''' +WhoisIAm.py - creation of Whois and IAm requests -Usage -Must be used while defining an app +Used while defining an app ex.: class BasicScript(WhoisIAm): Class : WhoisIAm -""" +''' +#--- standard Python modules --- +#--- 3rd party modules --- from bacpypes.debugging import bacpypes_debugging from bacpypes.apdu import WhoIsRequest, IAmRequest @@ -24,43 +25,38 @@ from bacpypes.object import get_object_class, get_datatype from bacpypes.iocb import IOCB +#--- this application's modules --- from ..functions.debug import log_debug, log_exception from ..io.IOExceptions import SegmentationNotSupported, ReadPropertyException, ReadPropertyMultipleException, NoResponseFromController, ApplicationNotStarted +#------------------------------------------------------------------------------ @bacpypes_debugging class WhoisIAm(): """ - This class will be used by inheritance to add features to an app - Will allows the usage of whois and iam functions + Define BACnet WhoIs and IAm functions. """ + def whois(self, *args): """ - Creation of a whois requests - Requets is given to the app + Build a WhoIs request :param args: string built as [ ] [ ] **optional** :returns: discoveredDevices as a defaultdict(int) Example:: - whois() - #will create a broadcast whois request and every device will response by a Iam - whois('2:5') - #will create a whois request aimed at device 5 - whois('10 1000') - #will create a whois request looking for device ID 10 to 1000 + whois() # WhoIs broadcast globally. Every device will respond with an IAm + whois('2:5') # WhoIs looking for the device at (Network 2, Address 5) + whois('10 1000') # WhoIs looking for devices in the ID range (10 - 1000) """ if not self._started: - raise ApplicationNotStarted('App not running, use startApp() function') + raise ApplicationNotStarted('BACnet stack not running - use startApp()') + if args: args = args[0].split() - - if not args: - msg = "any" - else: - msg = args + msg= arg if args else 'any' log_debug(WhoisIAm, "do_whois %r" % msg) @@ -79,21 +75,14 @@ def whois(self, *args): log_debug(WhoisIAm, " - request: %r" % request) - # make an IOCB - iocb = IOCB(request) - log_debug(WhoisIAm, " - iocb: %r", iocb) - - # give it to the application - self.this_application.request_io(iocb) - # give it to the application -# print(self.this_application) -# self.this_application.request(request) -# iocb = self.this_application.request(request) - iocb.wait() -# -# # do something for success -# if iocb.ioResponse: -# apdu = iocb.ioResponse + iocb = IOCB(request) # make an IOCB + + self.this_application.request_io(iocb) # pass to the BACnet stack + + iocb.wait() # Wait for BACnet response + + if iocb.ioResponse: # successful response + apdu = iocb.ioResponse # # should be an ack # if not isinstance(apdu, IAmRequest) and not isinstance(apdu, WhoIsRequest): # log_debug(WhoisIAm," - not an ack") @@ -132,15 +121,17 @@ def whois(self, *args): # # count the times this has been received # self.who_is_counter[key] += 1 - self.discoveredDevices = self.this_application.i_am_counter + if iocb.ioError: # unsuccessful: error/reject/abort + pass + self.discoveredDevices = self.this_application.i_am_counter return self.discoveredDevices + def iam(self): """ - Creation of a iam request - - Iam requests are sent when whois requests ask for it + Build an IAm response. IAm are sent in response to a WhoIs request that; + matches our device ID, whose device range includes us, or is a broadcast. Content is defined by the script (deviceId, vendor, etc...) :returns: bool @@ -153,25 +144,21 @@ def iam(self): log_debug(WhoisIAm, "do_iam") try: - # build a request + # build a response request = IAmRequest() request.pduDestination = GlobalBroadcast() - # set the parameters from the device object + # fill the response with details about us (from our device object) request.iAmDeviceIdentifier = self.this_device.objectIdentifier request.maxAPDULengthAccepted = self.this_device.maxApduLengthAccepted request.segmentationSupported = self.this_device.segmentationSupported request.vendorID = self.this_device.vendorIdentifier log_debug(WhoisIAm, " - request: %r" % request) - # give it to the application - iocb = self.this_application.request(request) + iocb = self.this_application.request(request) # pass to the BACnet stack iocb.wait() return True except Exception as error: log_exception("exception: %r" % error) return False - - - diff --git a/BAC0/core/functions/debug.py b/BAC0/core/functions/debug.py index a58a7121..2bdb5e4b 100644 --- a/BAC0/core/functions/debug.py +++ b/BAC0/core/functions/debug.py @@ -2,15 +2,22 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -Helper functions to log debug and exception messages +# +''' +debug.py - Helper functions to log debug and exception messages + +''' -""" +#--- standard Python modules --- from functools import wraps import inspect +#--- 3rd party modules --- +#--- this application's modules --- + +#------------------------------------------------------------------------------ + _DEBUG = 1 def debug(func): @@ -32,15 +39,13 @@ def wrapper(*args, debug=False, **kwargs): wrapper.__signature__ = sig.replace(parameters=parms) return wrapper + def log_debug(cls,txt, *args): """ Helper function to log debug messages """ if _DEBUG: - if args: - msg = txt % args - else: - msg = txt + msg= (txt % args) if args else txt # pylint: disable=E1101,W0212 cls._debug(msg) @@ -49,9 +54,6 @@ def log_exception(cls,txt, *args): """ Helper function to log debug messages """ - if args: - msg = txt % args - else: - msg = txt + msg= (txt % args) if args else txt # pylint: disable=E1101,W0212 cls._exception(msg) diff --git a/BAC0/core/functions/discoverPoints.py b/BAC0/core/functions/discoverPoints.py index a04e90a0..5706222e 100644 --- a/BAC0/core/functions/discoverPoints.py +++ b/BAC0/core/functions/discoverPoints.py @@ -2,24 +2,30 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -This module define discoverPoints function -""" +# +''' +discoverPoints.py - allow discovery of BACnet points in a controller. +''' -from ..devices.Points import EnumPoint, BooleanPoint, NumericPoint +#--- standard Python modules --- +#--- 3rd party modules --- try: import pandas as pd _PANDA = True except: _PANDA = False +#--- this application's modules --- +from ..devices.Points import EnumPoint, BooleanPoint, NumericPoint + + +#------------------------------------------------------------------------------ def discoverPoints(bacnetapp, address, devID): """ - This function allows the discovery of all bacnet points in a device + Discover the BACnet points in a BACnet device. :param bacnetApp: The app itself so we can call read :param address: address of the device as a string (ex. '2:5') @@ -27,52 +33,68 @@ def discoverPoints(bacnetapp, address, devID): :returns: a tuple with deviceName, pss, objList, df - *deviceName* : name of the device - *pss* : protocole service supported - *objList* : list of bacnet object (ex. analogInput, 1) - *df* : is a dataFrame containing pointType, pointAddress, pointName, description - presentValue and units - - If pandas can't be found, df will be a simple array + * *deviceName* : name of the device + * *pss* : protocole service supported + * *objList* : list of bacnet object (ex. analogInput, 1) + * *df* : is a dataFrame containing pointType, pointAddress, pointName, description + presentValue and units + If pandas can't be found, df will be a simple array """ - pss = bacnetapp.read( - '%s device %s protocolServicesSupported' % (address, devID)) - deviceName = bacnetapp.read('%s device %s objectName' % (address, devID)) - print('Found %s... building points list' % deviceName) - objList = bacnetapp.read('%s device %s objectList' % (address, devID)) + pss = bacnetapp.read('{} device {} protocolServicesSupported'.format(address, devID)) + deviceName = bacnetapp.read('{} device {} objectName'.format(address, devID)) + + print('Device {}- building points list'.format(deviceName)) + objList = bacnetapp.read('{} device {] objectList'.format(address, devID)) + newLine = [] result = [] points = [] for pointType, pointAddr in objList: - if pointType not in 'file calendar device schedule notificationClass eventLog': - if 'binary' not in pointType and 'multiState' not in pointType: - newLine = [pointType, pointAddr] - newLine.extend(bacnetapp.readMultiple( - '%s %s %s objectName description presentValue units' % (address, pointType, pointAddr))) - newPoint = NumericPoint(pointType=newLine[0], pointAddress=newLine[1], pointName=newLine[ - 2], description=newLine[3], presentValue=newLine[4], units_state=newLine[5]) - elif 'binary' in pointType: - newLine = [pointType, pointAddr] - infos = (bacnetapp.readMultiple( - '%s %s %s objectName description presentValue inactiveText activeText' % (address, pointType, pointAddr))) - newLine.extend(infos[:-2]) - newLine.extend([infos[-2:]]) - newPoint = BooleanPoint(pointType=newLine[0], pointAddress=newLine[1], pointName=newLine[ - 2], description=newLine[3], presentValue=newLine[4], units_state=newLine[5]) - elif 'multiState' in pointType: - newLine = [pointType, pointAddr] - newLine.extend(bacnetapp.readMultiple( - '%s %s %s objectName description presentValue stateText' % (address, pointType, pointAddr))) - newPoint = EnumPoint(pointType=newLine[0], pointAddress=newLine[1], pointName=newLine[ - 2], description=newLine[3], presentValue=newLine[4], units_state=newLine[5]) - result.append(newLine) - points.append(newPoint) + + if 'binary' in pointType: # BI/BO/BV + newLine = [pointType, pointAddr] + infos = bacnetapp.readMultiple( + '{} {} {} objectName description presentValue inactiveText activeText'.format( + address, pointType, pointAddr)) + + newLine.extend(infos[:-2]) + newLine.extend([infos[-2:]]) + newPoint = BooleanPoint(pointType=newLine[0], pointAddress=newLine[1], + pointName=newLine[2], description=newLine[3], + presentValue=newLine[4], units_state=newLine[5]) + + elif 'multiState' in pointType: # MI/MV/MO + newLine = [pointType, pointAddr] + newLine.extend(bacnetapp.readMultiple( + '{} {} {} objectName description presentValue stateText'.format(address, pointType, pointAddr))) + + newPoint = EnumPoint(pointType=newLine[0], pointAddress=newLine[1], + pointName=newLine[2], description=newLine[3], + presentValue=newLine[4], units_state=newLine[5]) + + elif 'analog' in pointType: # AI/AO/AV + newLine = [pointType, pointAddr] + newLine.extend(bacnetapp.readMultiple( + '{} {} {} objectName description presentValue units'.format(address, pointType, pointAddr))) + + newPoint = NumericPoint(pointType=newLine[0], pointAddress=newLine[1], + pointName=newLine[2], description=newLine[3], + presentValue=newLine[4], units_state=newLine[5]) + + else: + continue # skip + + result.append(newLine) + points.append(newPoint) + + if _PANDA: df = pd.DataFrame(result, columns=['pointType', 'pointAddress', 'pointName', 'description', 'presentValue', 'units_state']).set_index(['pointName']) else: df = result + print('Ready!') return (deviceName, pss, objList, df, points) diff --git a/BAC0/core/io/IOExceptions.py b/BAC0/core/io/IOExceptions.py index 79069064..1869aedd 100644 --- a/BAC0/core/io/IOExceptions.py +++ b/BAC0/core/io/IOExceptions.py @@ -2,12 +2,11 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -Definition of exceptions -""" - +# +''' +IOExceptions.py - BAC0 application level exceptions +''' class WritePropertyException(Exception): """ @@ -90,4 +89,4 @@ class BokehServerCantStart(Exception): class SegmentationNotSupported(Exception): - pass \ No newline at end of file + pass diff --git a/BAC0/core/io/Read.py b/BAC0/core/io/Read.py index baefe5b7..4283aa6f 100644 --- a/BAC0/core/io/Read.py +++ b/BAC0/core/io/Read.py @@ -2,13 +2,12 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -This module allows the creation of ReadProperty and ReadPropertyMultiple -requests by and app +# +''' +Read.py - creation of ReadProperty and ReadPropertyMultiple requests - Must be used while defining an app + Used while defining an app: Example:: class BasicScript(WhoisIAm, ReadProperty) @@ -18,26 +17,33 @@ class BasicScript(WhoisIAm, ReadProperty) ReadProperty() def read() def readMultiple() -""" +''' + +#--- standard Python modules --- +from queue import Queue, Empty +import time + +#--- 3rd party modules --- from bacpypes.debugging import bacpypes_debugging from bacpypes.pdu import Address from bacpypes.object import get_object_class, get_datatype from bacpypes.apdu import PropertyReference, ReadAccessSpecification, \ ReadPropertyRequest, ReadPropertyMultipleRequest + from bacpypes.basetypes import PropertyIdentifier from bacpypes.apdu import ReadPropertyMultipleACK, ReadPropertyACK from bacpypes.primitivedata import Unsigned from bacpypes.constructeddata import Array from bacpypes.iocb import IOCB -from queue import Queue, Empty -import time - +#--- this application's modules --- from .IOExceptions import SegmentationNotSupported, ReadPropertyException, ReadPropertyMultipleException, NoResponseFromController, ApplicationNotStarted from ..functions.debug import log_debug, log_exception +#------------------------------------------------------------------------------ + # some debugging _DEBUG = 0 @@ -45,11 +51,9 @@ def readMultiple() @bacpypes_debugging class ReadProperty(): """ - This class defines functions to read bacnet messages. - It handles readProperty, readPropertyMultiple + Defines BACnet Read functions: readProperty and readPropertyMultiple. Data exchange is made via a Queue object - A timeout of 5 seconds allow detection of invalid device or communciation - errors. + A timeout of 10 seconds allows detection of invalid device or communciation errors. """ _TIMEOUT = 10 @@ -62,8 +66,8 @@ class ReadProperty(): # self._started = False def read(self, args, arr_index = None): - """ This function build a read request wait for the answer and - return the value + """ + Build a ReadProperty request, wait for the answer and return the value :param args: String with [ ] :returns: data read from device (str representing data like 10 or True) @@ -75,39 +79,32 @@ def read(self, args, arr_index = None): bacnet = BAC0.ReadWriteScript(localIPAddr = myIPAddr) bacnet.read('2:5 analogInput 1 presentValue') - will read controller with a MAC address of 5 in the network 2 - Will ask for the present Value of analog input 1 (AI:1) + Requests the controller at (Network 2, address 5) for the presentValue of + its analog input 1 (AI:1). """ if not self._started: - raise ApplicationNotStarted('App not running, use startApp() function') + raise ApplicationNotStarted('BACnet stack not running - use startApp()') #with self.this_application._lock: #time.sleep(0.5) #self.this_application._lock = True + args = args.split() - #self.this_application.value is None log_debug(ReadProperty, "do_read %r", args) try: - iocb = IOCB(self.build_rp_request(args, arr_index)) - # give it to the application - self.this_application.request_io(iocb) - #print('iocb : ', iocb) + iocb = IOCB(self.build_rp_request(args, arr_index)) # build ReadProperty request + self.this_application.request_io(iocb) # pass to the BACnet stack log_debug(ReadProperty," - iocb: %r", iocb) - except ReadPropertyException as error: - # error in the creation of the request - log_exception("exception: %r", error) - - # Wait for the response - iocb.wait() - - # do something for success - if iocb.ioResponse: + log_exception("exception: %r", error) # construction error + + iocb.wait() # Wait for BACnet response + + if iocb.ioResponse: # successful response apdu = iocb.ioResponse - # should be an ack - if not isinstance(apdu, ReadPropertyACK): + if not isinstance(apdu, ReadPropertyACK): # expecting an ACK log_debug(ReadProperty," - not an ack") return @@ -127,11 +124,9 @@ def read(self, args, arr_index = None): value = apdu.propertyValue.cast_out(datatype) log_debug(ReadProperty," - value: %r", value) - return value - # do something for error/reject/abort - if iocb.ioError: + if iocb.ioError: # unsuccessful: error/reject/abort raise NoResponseFromController() # Share response with Queue @@ -152,9 +147,9 @@ def read(self, args, arr_index = None): # #self.this_application._lock = False # raise NoResponseFromController() + def readMultiple(self, args): - """ This function build a readMultiple request wait for the answer and - return the value + """ Build a ReadPropertyMultiple request, wait for the answer and return the values :param args: String with ( ( [ ] )... )... :returns: data read from device (str representing data like 10 or True) @@ -166,32 +161,30 @@ def readMultiple(self, args): bacnet = BAC0.ReadWriteScript(localIPAddr = myIPAddr) bacnet.readMultiple('2:5 analogInput 1 presentValue units') - will read controller with a MAC address of 5 in the network 2 - Will ask for the present Value and the units of analog input 1 (AI:1) + Requests the controller at (Network 2, address 5) for the (presentValue and units) of + its analog input 1 (AI:1). """ if not self._started: - raise ApplicationNotStarted('App not running, use startApp() function') + raise ApplicationNotStarted('BACnet stack not running - use startApp()') args = args.split() values = [] log_debug(ReadProperty, "readMultiple %r", args) try: - iocb = IOCB(self.build_rpm_request(args)) - # give it to the application - self.this_application.request_io(iocb) + iocb = IOCB(self.build_rpm_request(args)) # build an ReadPropertyMultiple request + self.this_application.request_io(iocb) # pass to the BACnet stack except ReadPropertyMultipleException as error: - log_exception(ReadProperty, "exception: %r", error) + log_exception("exception: %r", error) # construction error + - iocb.wait() + iocb.wait() # Wait for BACnet response - # do something for success - if iocb.ioResponse: + if iocb.ioResponse: # successful response apdu = iocb.ioResponse - # should be an ack - if not isinstance(apdu, ReadPropertyMultipleACK): + if not isinstance(apdu, ReadPropertyMultipleACK): # expecting an ACK log_debug(ReadProperty," - not an ack") return @@ -209,16 +202,13 @@ def readMultiple(self, args): propertyArrayIndex = element.propertyArrayIndex log_debug(ReadProperty," - propertyArrayIndex: %r", propertyArrayIndex) - # here is the read result readResult = element.readResult if propertyArrayIndex is not None: print("[" + str(propertyArrayIndex) + "]") - # check for an error if readResult.propertyAccessError is not None: print(" ! " + str(readResult.propertyAccessError)) - else: # here is the value propertyValue = readResult.propertyValue @@ -240,11 +230,11 @@ def readMultiple(self, args): log_debug(ReadProperty," - value: %r", value) values.append(value) + return values - # do something for error/reject/abort - if iocb.ioError: + if iocb.ioError: # unsuccessful: error/reject/abort raise NoResponseFromController() # data = None @@ -291,6 +281,7 @@ def build_rp_request(self, args, arr_index = None): return request + def build_rpm_request(self, args): """ Build request from args @@ -329,41 +320,32 @@ def build_rpm_request(self, args): (obj_type, prop_id)) # build a property reference - prop_reference = PropertyReference( - propertyIdentifier=prop_id, - ) + prop_reference = PropertyReference(propertyIdentifier=prop_id) # check for an array index if (i < len(args)) and args[i].isdigit(): prop_reference.propertyArrayIndex = int(args[i]) i += 1 - # 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") # build a read access specification read_access_spec = ReadAccessSpecification( objectIdentifier=(obj_type, obj_inst), - listOfPropertyReferences=prop_reference_list, - ) + listOfPropertyReferences=prop_reference_list ) - # add it to the list read_access_spec_list.append(read_access_spec) - # check for at least one if not read_access_spec_list: raise RuntimeError( "at least one read access specification required") # build the request - request = ReadPropertyMultipleRequest( - listOfReadAccessSpecs=read_access_spec_list, - ) + request = ReadPropertyMultipleRequest(listOfReadAccessSpecs=read_access_spec_list ) request.pduDestination = Address(addr) log_debug(ReadProperty, " - request: %r", request) - + return request diff --git a/BAC0/core/io/Simulate.py b/BAC0/core/io/Simulate.py index 9cf44419..402e1c04 100644 --- a/BAC0/core/io/Simulate.py +++ b/BAC0/core/io/Simulate.py @@ -2,77 +2,98 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -This module define a way to simulate value of IO variables -""" +# +''' +Simulate.py - simulate the value of controller I/O values +''' +#--- standard Python modules --- +#--- 3rd party modules --- +#--- this application's modules --- from .IOExceptions import OutOfServiceNotSet, OutOfServiceSet, NoResponseFromController, ApplicationNotStarted +#------------------------------------------------------------------------------ + class Simulation(): """ Global informations regarding simulation """ + def sim(self, args): """ - This function allow the simulation of IO points by turning on the - out_of_service property. When out_of_service, write the value to - the point + Simulate I/O points by setting the Out_Of_Service property, then doing a + WriteProperty to the point's Present_Value. :param args: String with [ ] [ ] """ if not self._started: - raise ApplicationNotStarted('App not running, use startApp() function') + raise ApplicationNotStarted('BACnet stack not running - use startApp()') + #with self.this_application._lock: if use lock...won't be able to call read... args = args.split() addr, obj_type, obj_inst, prop_id, value = args[:5] - if self.read('%s %s %s outOfService' % (addr, obj_type, obj_inst)): - self.write( - '%s %s %s %s %s' % - (addr, obj_type, obj_inst, prop_id, value)) + + if self.read('{} {} {} outOfService'.format(addr, obj_type, obj_inst)): + self.write('{} {} {} {} {}'.format(addr, obj_type, obj_inst, prop_id, value)) else: try: - self.write( - '%s %s %s outOfService True' % - (addr, obj_type, obj_inst)) + self.write('{} {} {} outOfService True'.format(addr, obj_type, obj_inst)) except NoResponseFromController: pass + try: - if self.read('%s %s %s outOfService' % - (addr, obj_type, obj_inst)): - self.write('%s %s %s %s %s' % - (addr, obj_type, obj_inst, prop_id, value)) + if self.read('{} {} {} outOfService'.format(addr, obj_type, obj_inst)): + self.write('{} {} {} {} {}'.format(addr, obj_type, obj_inst, prop_id, value)) else: raise OutOfServiceNotSet() except NoResponseFromController: pass + + def out_of_service(self, args): + """ + Set the Out_Of_Service property so the Present_Value of an I/O may be written. + + :param args: String with [ ] [ ] + + """ + if not self._started: + raise ApplicationNotStarted('BACnet stack not running - use startApp()') + + #with self.this_application._lock: if use lock...won't be able to call read... + args = args.split() + addr, obj_type, obj_inst = args[:3] + try: + self.write('{} {} {} outOfService True'.format(addr, obj_type, obj_inst)) + except NoResponseFromController: + pass + + def release(self, args): """ - This function will turn out_of_service property to false so the - point will resume it's normal behaviour + Set the Out_Of_Service property to False - to release the I/O point back to + the controller's control. :param args: String with """ if not self._started: - raise ApplicationNotStarted('App not running, use startApp() function') + raise ApplicationNotStarted('BACnet stack not running - use startApp()') + args = args.split() addr, obj_type, obj_inst = args[:3] try: - self.write( - '%s %s %s outOfService False' % - (addr, obj_type, obj_inst)) + self.write('{} {} {} outOfService False'.format(addr, obj_type, obj_inst)) except NoResponseFromController: pass + try: - if self.read('%s %s %s outOfService' % (addr, obj_type, obj_inst)): + if self.read('{} {} {} outOfService'.format(addr, obj_type, obj_inst)): raise OutOfServiceSet() else: - "Everything is ok" - pass + pass # Everything is ok" except NoResponseFromController: pass diff --git a/BAC0/core/io/Write.py b/BAC0/core/io/Write.py index 9e88a7d4..00c0cf28 100644 --- a/BAC0/core/io/Write.py +++ b/BAC0/core/io/Write.py @@ -2,12 +2,12 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -This module allows the creation of WriteProperty requests by and app +# +''' +Write.py - creation of WriteProperty requests - Must be used while defining an app + Used while defining an app Example:: class BasicScript(WhoisIAm, WriteProperty) @@ -21,9 +21,13 @@ def write() print_debug() -""" -from bacpypes.debugging import bacpypes_debugging, ModuleLogger +''' +#--- standard Python modules --- +from queue import Empty +import time +#--- 3rd party modules --- +from bacpypes.debugging import bacpypes_debugging, ModuleLogger from bacpypes.pdu import Address from bacpypes.object import get_datatype @@ -34,13 +38,13 @@ def write() from bacpypes.constructeddata import Array, Any from bacpypes.iocb import IOCB -from queue import Empty -import time - +#--- this application's modules --- from .IOExceptions import WritePropertyCastError, NoResponseFromController, WritePropertyException, WriteAccessDenied, ApplicationNotStarted from ..functions.debug import log_debug, log_exception +#------------------------------------------------------------------------------ + # some debugging _debug = 0 _LOG = ModuleLogger(globals()) @@ -49,20 +53,15 @@ def write() @bacpypes_debugging class WriteProperty(): """ - This class define function to write to bacnet objects - Will implement a Queue object waiting for an acknowledgment - """ - """ - This class defines functions to write to bacnet properties. - It handles writeProperty - Data exchange is made via a Queue object - A timeout of 2 seconds allow detection of invalid device or communciation - errors. + Defines BACnet Write functions: WriteProperty [WritePropertyMultiple not supported] + + A timeout of 10 seconds allows detection of invalid device or communciation errors. """ _TIMEOUT = 10 + + def write(self, args): - """ This function build a write request wait for an acknowledgment and - return a boolean status (True if ok, False if not) + """ Build a WriteProperty request, wait for an answer, and return status [True if ok, False if not]. :param args: String with [ ] [ ] :returns: data read from device (str representing data like 10 or True) @@ -74,10 +73,11 @@ def write(self, args): bacnet = BAC0.ReadWriteScript(localIPAddr = myIPAddr) bacnet.write('2:5 analogValue 1 presentValue 100') - will write 100 to AV:1 of a controller with a MAC address of 5 in the network 2 + Direct the controller at (Network 2, address 5) to write 100 to the presentValues of + its analogValue 1 (AV:1) """ if not self._started: - raise ApplicationNotStarted('App not running, use startApp() function') + raise ApplicationNotStarted('BACnet stack not running - use startApp()') #with self.this_application._lock: # time.sleep(0.5) #self.this_application._lock = True @@ -85,29 +85,23 @@ def write(self, args): log_debug(WriteProperty, "do_write %r", args) try: - iocb = IOCB(self.build_wp_request(args)) - # give it to the application - self.this_application.request_io(iocb) + iocb = IOCB(self.build_wp_request(args)) # build a WriteProperty request + self.this_application.request_io(iocb) # pass to the BACnet stack except WritePropertyException as error: - log_exception("exception: %r", error) + log_exception("exception: %r", error) # construction error - # wait for it to complete - iocb.wait() + iocb.wait() # Wait for BACnet response - # do something for success - if iocb.ioResponse: - # should be an ack - if not isinstance(iocb.ioResponse, SimpleAckPDU): - #if _debug: ReadWritePropertyConsoleCmd._debug(" - not an ack") + if iocb.ioResponse: # successful response + if not isinstance(iocb.ioResponse, SimpleAckPDU): # expect an ACK + log_debug(WriteProperty," - not an ack") return - #sys.stdout.write("ack\n") - - # do something for error/reject/abort - if iocb.ioError: + if iocb.ioError: # unsuccessful: error/reject/abort raise NoResponseFromController() + # while True: # try: # data, evt = self.this_application.ResponseQueue.get( @@ -119,6 +113,7 @@ def write(self, args): # #self.this_application._lock = False # raise NoResponseFromController + def build_wp_request(self, args): addr, obj_type, obj_inst, prop_id = args[:4] if obj_type.isdigit(): @@ -145,6 +140,7 @@ def build_wp_request(self, args): # case if value == 'null': value = Null() + elif issubclass(datatype, Atomic): if datatype is Integer: value = int(value) @@ -153,6 +149,7 @@ def build_wp_request(self, args): elif datatype is Unsigned: value = int(value) value = datatype(value) + elif issubclass(datatype, Array) and (indx is not None): if indx == 0: value = Integer(value) @@ -162,6 +159,7 @@ def build_wp_request(self, args): raise TypeError( "invalid result datatype, expecting %s" % (datatype.subtype.__name__,)) + elif not isinstance(value, datatype): raise TypeError( "invalid result datatype, expecting %s" % @@ -174,9 +172,7 @@ def build_wp_request(self, args): # build a request request = WritePropertyRequest( - objectIdentifier=(obj_type, obj_inst), - propertyIdentifier=prop_id - ) + objectIdentifier=(obj_type, obj_inst), propertyIdentifier=prop_id ) request.pduDestination = Address(addr) # save the value @@ -195,4 +191,4 @@ def build_wp_request(self, args): request.priority = priority log_debug(WriteProperty, " - request: %r", request) - return request \ No newline at end of file + return request diff --git a/BAC0/infos.py b/BAC0/infos.py index 34b90d89..dcc1dddc 100644 --- a/BAC0/infos.py +++ b/BAC0/infos.py @@ -2,15 +2,15 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -Informations MetaData -""" +# +''' +infos.py - BAC0 Package MetaData +''' __author__ = 'Christian Tremblay, P.Eng.' __email__ = 'christian.tremblay@servisys.com' __url__ = 'https://github.com/ChristianTremblay/BAC0' __download_url__ = 'https://github.com/ChristianTremblay/BAC0/archive/master.zip' __version__ = '0.99.82' -__license__ = 'LGPLv3' \ No newline at end of file +__license__ = 'LGPLv3' diff --git a/BAC0/scripts/BasicScript.py b/BAC0/scripts/BasicScript.py index 1c25e49f..58efd13f 100644 --- a/BAC0/scripts/BasicScript.py +++ b/BAC0/scripts/BasicScript.py @@ -2,118 +2,115 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. +# """ -BasicScript is an object that will implement the BAC0.core.app.ScriptApplication -The object will also implement basic function to start and stop this application. -Stopping the app allows to free the socket used by the app. -No communication will occur if the app is stopped. - -The basic script will be able to use basic Whois and Iam function. Those features -are allowed by inheritance as the class will extend WhoisIAm class. +BasicScript - implement the BAC0.core.app.ScriptApplication +Its basic function is to start and stop the bacpypes stack. +Stopping the stack, frees the IP socket used for BACnet communications. +No communications will occur if the stack is stopped. -Adding other features will work the same way (see BAC0.scripts.ReadWriteScript) +Bacpypes stack enables Whois and Iam functions, since this minimum is needed to be +a BACnet device. Other stack services can be enabled later (via class inheritance). +[see: see BAC0.scripts.ReadWriteScript] Class:: - BasicScript(WhoisIAm) def startApp() def stopApp() - """ +#--- standard Python modules --- +from threading import Thread +from queue import Queue +import random +import sys + +#--- 3rd party modules --- +import pandas as pd from bacpypes.debugging import bacpypes_debugging, ModuleLogger + from bacpypes.core import run as startBacnetIPApp from bacpypes.core import stop as stopBacnetIPApp from bacpypes.core import enable_sleeping + from bacpypes.service.device import LocalDeviceObject from bacpypes.basetypes import ServicesSupported, DeviceStatus from bacpypes.primitivedata import CharacterString -from threading import Thread -import pandas as pd - -from queue import Queue -import random -import sys - +#--- this application's modules --- from ..core.app.ScriptApplication import ScriptApplication from .. import infos from ..core.io.IOExceptions import NoResponseFromController #import BAC0.core.functions as fn +#------------------------------------------------------------------------------ -# some debugging -_DEBUG = 0 +_DEBUG = 1 @bacpypes_debugging class BasicScript(): """ - This class build a running bacnet application and will accept whois ans iam requests - + Build a running BACnet/IP device that accepts WhoIs and IAm requests + Initialization requires some minimial information about the local device. + + :param localIPAddr='127.0.0.1': + :param localObjName='BAC0': + :param DeviceId=None: + :param maxAPDULengthAccepted='1024': + :param maxSegmentsAccepted='1024': + :param segmentationSupported='segmentedBoth': """ - - def __init__(self, localIPAddr=None, localObjName='BAC0', Boid=None, - maxAPDULengthAccepted='1024', segmentationSupported='segmentedBoth'): - """ - Initialization requires information about the local device - Default values are localObjName = 'name', Boid = '2015',maxAPDULengthAccepted = '1024',segmentationSupported = 'segmentedBoth', vendorId = '842' ) - Local IP address must be given in a string. - Normally, the address must be in the same subnet than the bacnet network (if no BBMD or Foreign device is used) - Script doesn't support BBMD actually - """ - log_debug("Configurating app") + def __init__(self, localIPAddr='127.0.0.1', localObjName='BAC0', DeviceId=None, + maxAPDULengthAccepted='1024', maxSegmentsAccepted='1024', segmentationSupported='segmentedBoth'): + log_debug("Configure App") self.response = None self._initialized = False self._started = False self._stopped = False - if localIPAddr: - self.localIPAddr = localIPAddr - else: - self.localIPAddr = '127.0.0.1' + + self.localIPAddr= localIPAddr + self.Boid = int(DeviceId) if DeviceId else (3056177 + int(random.uniform(0, 1000))) + self.segmentationSupported = segmentationSupported + self.maxSegmentsAccepted = maxSegmentsAccepted self.localObjName = localObjName - if Boid: - self.Boid = int(Boid) - else: - self.Boid = int('3056177') + int(random.uniform(0, 1000)) + self.maxAPDULengthAccepted = maxAPDULengthAccepted - self.vendorId = '842' + self.vendorId = 842 self.vendorName = CharacterString('SERVISYS inc.') self.modelName = CharacterString('BAC0 Scripting Tool') + self.discoveredDevices = None - #self.ResponseQueue = Queue() self.systemStatus = DeviceStatus(1) - #self.this_application = None self.startApp() + def startApp(self): """ - This function is used to define the local device, including services supported. - Once the application is defined, calls the _startAppThread which will handle the thread creation + Define the local device, including services supported. + Once defined, start the BACnet stack in its own thread. """ - log_debug("App initialization") + log_debug("Create Local Device") try: # make a device object self.this_device = LocalDeviceObject( - objectName=self.localObjName, - objectIdentifier=int(self.Boid), + objectName= self.localObjName, + objectIdentifier= self.Boid, maxApduLengthAccepted=int(self.maxAPDULengthAccepted), segmentationSupported=self.segmentationSupported, - vendorIdentifier=int(self.vendorId), - vendorName=self.vendorName, - modelName=self.modelName, - systemStatus=self.systemStatus, + vendorIdentifier= self.vendorId, + vendorName= self.vendorName, + modelName= self.modelName, + systemStatus= self.systemStatus, description='http://christiantremblay.github.io/BAC0/', firmwareRevision=''.join(sys.version.split('|')[:2]), applicationSoftwareVersion=infos.__version__, protocolVersion=1, protocolRevision=0, - ) # build a bit string that knows about the bit names @@ -128,52 +125,50 @@ def startApp(self): self.this_device.protocolServicesSupported = pss.value # make a simple application - self.this_application = ScriptApplication( - self.this_device, self.localIPAddr) + self.this_application = ScriptApplication(self.this_device, self.localIPAddr) log_debug("Starting") self._initialized = True self._startAppThread() log_debug("Running") + except Exception as error: log_exception("an error has occurred: %s", error) finally: log_debug("finally") + def disconnect(self): """ - Used to stop the application - Free the socket using ``handle_close()`` function - Stop the thread + Stop the BACnet stack. Free the IP socket. """ - print('Stopping app') + print('Stopping BACnet stack') # Freeing socket try: self.this_application.mux.directPort.handle_close() except: self.this_application.mux.broadcastPort.handle_close() - # Stopping Core - stopBacnetIPApp() - self._stopped = True - # Stopping thread - # print(Thread.is_alive) + stopBacnetIPApp() # Stop Core + self._stopped = True # Stop stack thread self.t.join() self._started = False - print('App stopped') + print('BACnet stopped') + def _startAppThread(self): """ - Starts the application in its own thread so requests can be processed. - Once started, socket will be reserved. + Starts the BACnet stack in its own thread so requests can be processed. """ print('Starting app...') enable_sleeping(0.0005) - self.t = Thread(target=startBacnetIPApp, daemon = True) + self.t = Thread(target=startBacnetIPApp, name='bacnet', daemon = True) + #self.t = Thread(target=startBacnetIPApp, kwargs={'sigterm': None,'sigusr1': None}, daemon = True) self.t.start() self._started = True - print('App started') + print('BACnet started') + @property def devices(self): lst = [] @@ -191,25 +186,15 @@ def devices(self): def log_debug(txt, *args): - """ - Helper function to log debug messages + """ Helper function to log debug messages """ if _DEBUG: - if args: - msg = txt % args - else: - msg = txt - # pylint: disable=E1101,W0212 + msg= (txt % args) if args else txt BasicScript._debug(msg) def log_exception(txt, *args): + """ Helper function to log debug messages """ - Helper function to log debug messages - """ - if args: - msg = txt % args - else: - msg = txt - # pylint: disable=E1101,W0212 + msg= (txt % args) if args else txt BasicScript._exception(msg) diff --git a/BAC0/scripts/ReadWriteScript.py b/BAC0/scripts/ReadWriteScript.py index 08fb2b77..05b85b7a 100644 --- a/BAC0/scripts/ReadWriteScript.py +++ b/BAC0/scripts/ReadWriteScript.py @@ -2,42 +2,47 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -This Script object is an extended version of the basicScript. -As everything is handled by the BasicScript, you only need to select the features -you want:: +# +''' +ReadWriteScript - extended version of BasicScript.py - # Create a class that will implement a basic script with read and write functions +As everything is handled by the BasicScript, select the additional features you want:: + + # Create a class that implements a basic script with read and write functions from BAC0.scripts.BasicScript import BasicScript from BAC0.core.io.Read import ReadProperty from BAC0.core.io.Write import WriteProperty class ReadWriteScript(BasicScript,ReadProperty,WriteProperty) -Once the class is created, create the object and use it:: +Once the class is created, create the local object and use it:: bacnet = ReadWriteScript(localIPAddr = '192.168.1.10') bacnet.read('2:5 analogInput 1 presentValue) -""" +''' +#--- standard Python modules --- +import requests +import time +import logging +#--- 3rd party modules --- from bacpypes.debugging import bacpypes_debugging +#--- this application's modules --- from ..scripts.BasicScript import BasicScript + from ..core.io.Read import ReadProperty from ..core.io.Write import WriteProperty from ..core.functions.GetIPAddr import HostIP from ..core.functions.WhoisIAm import WhoisIAm from ..core.io.Simulate import Simulation from ..core.io.IOExceptions import BokehServerCantStart + from ..bokeh.BokehRenderer import BokehSession, BokehDocument from ..bokeh.BokehServer import BokehServer - -import requests -import time -import logging +#------------------------------------------------------------------------------ # some debugging _DEBUG = 0 @@ -46,41 +51,35 @@ class ReadWriteScript(BasicScript,ReadProperty,WriteProperty) @bacpypes_debugging class ReadWriteScript(BasicScript, WhoisIAm, ReadProperty, WriteProperty, Simulation): """ - This class build a running bacnet application and will accept read ans write requests - Whois and IAm function are also possible as they are implemented in the BasicScript class. - - Once created, the object will call a ``whois()`` function to build a list of controllers available. + Build a BACnet application to accept read and write requests. + [Basic Whois/IAm functions are implemented in parent BasicScript class.] + Once created, execute a whois() to build a list of available controllers. + Initialization requires information on the local device. + :param ip='127.0.0.1': Address must be in the same subnet as the BACnet network + [BBMD and Foreign Device - not supported] """ - def __init__(self, ip=None): - """ - Initialization requires information on the local device - - :param ip: (str) '127.0.0.1' - - Normally, the address must be in the same subnet than the bacnet network (if no BBMD or Foreign device is used) - Actual app doesn't support BBMD or FD - - You need to pass the args to the parent BasicScript - - """ log_debug("Configurating app") if ip is None: host = HostIP() ip_addr = host.address else: ip_addr = ip + BasicScript.__init__(self, localIPAddr=ip_addr) + self.bokehserver = False # Force and global whois to find all devices on the network self.whois() self.start_bokeh() + def start_bokeh(self): try: print('Starting Bokeh Serve') logging.getLogger("requests").setLevel(logging.INFO) + self.BokehServer = BokehServer() self.BokehServer.start() attemptedConnections = 0 @@ -98,18 +97,20 @@ def start_bokeh(self): self.bokeh_document = BokehDocument(title = 'BAC0 - Live Trending') self.new_bokeh_session() self.bokeh_session.loop() + except OSError as error: self.bokehserver = False - print('Please start bokeh serve to use trending features') - print('controller.chart will not work') + print('[bokeh serve] required for trending (controller.chart) features') print(error) + except RuntimeError as rterror: self.bokehserver = False print('Server already running') + except BokehServerCantStart: self.bokehserver = False - print("Can't start Bokeh Server") - print('controller.chart will not work') + print('No Bokeh Server - controller.chart not available') + def new_bokeh_session(self): self.bokeh_session = BokehSession(self.bokeh_document.document) @@ -119,25 +120,15 @@ def __repr__(self): def log_debug(txt, *args): - """ - Helper function to log debug messages + """ Helper function to log debug messages """ if _DEBUG: - if args: - msg = txt % args - else: - msg = txt - # pylint: disable=E1101,W0212 + msg= (txt % args) if args else txt BasicScript._debug(msg) def log_exception(txt, *args): + """ Helper function to log debug messages """ - Helper function to log debug messages - """ - if args: - msg = txt % args - else: - msg = txt - # pylint: disable=E1101,W0212 + msg= (txt % args) if args else txt BasicScript._exception(msg) diff --git a/BAC0/scripts/__init__.py b/BAC0/scripts/__init__.py index 3bf46b19..cc4715d9 100644 --- a/BAC0/scripts/__init__.py +++ b/BAC0/scripts/__init__.py @@ -1,9 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -#!/usr/bin/python # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. +# from . import BasicScript from . import ReadWriteScript \ No newline at end of file diff --git a/BAC0/sql/sql.py b/BAC0/sql/sql.py index 8e4e1ac7..e10d7fec 100644 --- a/BAC0/sql/sql.py +++ b/BAC0/sql/sql.py @@ -2,21 +2,33 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. +# +''' +sql.py - +''' + +#--- standard Python modules --- +import pickle +import os.path +#--- 3rd party modules --- import sqlite3 import pandas as pd from pandas.io import sql from pandas.lib import Timestamp -import pickle -import os.path +#--- this application's modules --- + +#------------------------------------------------------------------------------ class SQLMixin(object): """ - A mixin to be used with a Device to backup to SQL. + Use SQL to persist a device's contents. By saving the device contents to an SQL + database, you can work with the device's data while offline, or while the device + is not available. """ + def dev_properties_df(self): dic = self.properties.asdict.copy() dic.pop('charts', None) @@ -28,7 +40,7 @@ def dev_properties_df(self): def points_properties_df(self): """ - This returns a dict of point / point_properties so it can be saved to SQL + Return a dictionary of point/point_properties in preparation for storage in SQL. """ pprops = {} for each in self.points: @@ -39,13 +51,14 @@ def points_properties_df(self): p.pop('simulated', None) p.pop('overridden', None) pprops[each.properties.name] = p - pprops + df = pd.DataFrame(pprops) return df + def backup_histories_df(self): """ - Build a dataframe from all histories + Build a dataframe of the point histories """ backup = {} for point in self.points: @@ -53,33 +66,35 @@ def backup_histories_df(self): backup[point.properties.name] = point.history.replace(['inactive', 'active'], [0, 1]).resample('1s').mean() else: backup[point.properties.name] = point.history.resample('1s').mean() + # in some circumstances, correct : pd.DataFrame(dict([ (k,pd.Series(v)) for k,v in backup.items() ])) backup = pd.DataFrame(dict([ (k,pd.Series(v)) for k,v in backup.items() ])) return pd.DataFrame(backup) + def save(self, filename = None): """ - Will save histories to sqlite3 file - Will also save device properties to a pickle file so the device - can be rebuilt + Save the point histories to sqlite3 database. + Save the device object properties to a pickle file so the device can be reloaded. """ if filename: self.properties.db_name = filename else: self.properties.db_name = self.properties.name - - # Do file exist ? If so...will nee to append data + # Does file exist? If so, append data if os.path.isfile('%s.db' % (self.properties.db_name)): print('File exists, appending data...') + db = sqlite3.connect('%s.db' % (self.properties.db_name)) his = sql.read_sql('select * from "%s"' % 'history', db) his.index = his['index'].apply(Timestamp) last = his.index[-1] df_to_backup = self.backup_histories_df()[last:] db.close() + else: - print('Will create a new file') + print('Creating a new backup database') df_to_backup = self.backup_histories_df() cnx = sqlite3.connect('%s.db' % (self.properties.db_name)) @@ -95,40 +110,43 @@ def save(self, filename = None): print('%s saved to disk' % self.properties.db_name) + def points_from_sql(self, db): """ - Function to retrieve points from DB + Retrieve point list from SQL database """ points = sql.read_sql("SELECT * FROM history;", db) return list(points.columns.values)[1:] + def his_from_sql(self, db, point): """ - Function to retrive histories from DB + Retrive point histories from SQL database """ his = sql.read_sql('select * from "%s"' % 'history', db) his.index = his['index'].apply(Timestamp) return his.set_index('index')[point] + def value_from_sql(self, db, point): """ Take last known value as the value """ return self.his_from_sql(db, point).last_valid_index() + def read_point_prop(self, device_name, point): """ Points properties retrieved from pickle """ with open( "%s.bin" % device_name, "rb" ) as file: return pickle.load(file)['points'][point] - + + def read_dev_prop(self, device_name): """ Device properties retrieved from pickle """ with open( "%s.bin" % device_name, "rb" ) as file: return pickle.load(file)['device'] - - - \ No newline at end of file + \ No newline at end of file diff --git a/BAC0/tasks/DoOnce.py b/BAC0/tasks/DoOnce.py index 4dd7f5b5..2aa5a229 100644 --- a/BAC0/tasks/DoOnce.py +++ b/BAC0/tasks/DoOnce.py @@ -2,10 +2,10 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. +# """ -Repeat read function every delay +DoOnce.py - execute a task once """ from .TaskManager import OneShotTask @@ -33,4 +33,4 @@ def __init__(self, fnc): raise ValueError('You must pass a function to this...') def task(self): - self.func() \ No newline at end of file + self.func() diff --git a/BAC0/tasks/Match.py b/BAC0/tasks/Match.py index d5cda970..50693c3c 100644 --- a/BAC0/tasks/Match.py +++ b/BAC0/tasks/Match.py @@ -2,18 +2,26 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -Change Fan status based on Fan Command -""" +# +''' +Match.py - verify a point's status matches its commanded value. + +Example: + Is a fan commanded to 'On' actually 'running'? +''' +#--- standard Python modules --- +#--- 3rd party modules --- + +#--- this application's modules --- from .TaskManager import Task +#------------------------------------------------------------------------------ class Match(Task): """ - Will match 2 points (ex. a status with a command) + Match two properties of a BACnet Object (i.e. a point status with its command). """ def __init__(self, command = None, status = None, delay=5): @@ -21,11 +29,43 @@ def __init__(self, command = None, status = None, delay=5): self.status = status Task.__init__(self, delay=delay, daemon = True) + def task(self): if self.status.history[-1] != self.command.history[-1]: self.status._setitem(self.command.history[-1]) - + + def stop(self): self.status._setitem('auto') self.exitFlag = True + +class Match_Value(Task): + """ + Verify a point's Present_Value equals the given value after a delay of X seconds. + Thus giving the BACnet controller (and connected equipment) time to respond to the + command. + + Match_Value(On, , 5) + + i.e. Does Fan value = On after 5 seconds. + """ + + def __init__(self, value = None, point = None, delay=5): + self.value = value + self.point = point + Task.__init__(self, delay=delay, daemon = True) + + + def task(self): + if hasattr(self.value, '__call__'): + value = self.value() + else: + value = self.value + if value != self.point: + self.point._setitem(value) + + + def stop(self): + self.status._setitem('auto') + self.exitFlag = True diff --git a/BAC0/tasks/Poll.py b/BAC0/tasks/Poll.py index 0e09fe23..061805e7 100644 --- a/BAC0/tasks/Poll.py +++ b/BAC0/tasks/Poll.py @@ -2,18 +2,25 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -Repeat read function every delay -""" +# +''' +Poll.py - create a Polling task to repeatedly read a point. +''' -from .TaskManager import Task +#--- standard Python modules --- + +#--- 3rd party modules --- from bacpypes.core import deferred +#--- this application's modules --- +from .TaskManager import Task + +#------------------------------------------------------------------------------ + class SimplePoll(Task): """ - Start a polling task which is in fact a recurring read of the point. + Start a polling task to repeatedly read a point's Present_Value. ex. device['point_name'].poll(delay=60) """ @@ -31,17 +38,18 @@ def __init__(self, point, *, delay=10): delay = 5 if point.properties: self._point = point - Task.__init__(self, delay=delay) + Task.__init__(self, name='rp_poll', delay=delay) else: - raise ValueError('Should provide a point object') + raise ValueError('Provide a point object') def task(self): self._point.value + class DevicePoll(Task): """ - Start a polling task which is in fact a recurring read of - a list of point using read property multiple. + Start a polling task to repeatedly read a list of points from a device using + ReadPropertyMultiple requests. """ def __init__(self, device, delay=10): @@ -56,7 +64,8 @@ def __init__(self, device, delay=10): if delay < 5: delay = 5 self._device = device - Task.__init__(self, delay=delay, daemon = True) + Task.__init__(self, name='rpm_poll', delay=delay, daemon = True) + def task(self): - self._device.read_multiple(list(self._device.points_name), points_per_request=25) \ No newline at end of file + self._device.read_multiple(list(self._device.points_name), points_per_request=25) diff --git a/BAC0/tasks/TaskManager.py b/BAC0/tasks/TaskManager.py index 315869a4..244ad121 100644 --- a/BAC0/tasks/TaskManager.py +++ b/BAC0/tasks/TaskManager.py @@ -2,15 +2,21 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2015 by Christian Tremblay, P.Eng -# # Licensed under LGPLv3, see file LICENSE in this source tree. -""" -This module allows the creation of threads that will be used as repetitive -tasks for simulation purposes -""" +# +''' +TaskManager.py - creation of threads used for repetitive tasks. + +A key building block for point simulation. +''' +#--- standard Python modules --- from threading import Thread, Lock import time +#--- 3rd party modules --- +#--- this application's modules --- + +#------------------------------------------------------------------------------ class Manager(): taskList = [] @@ -25,17 +31,19 @@ def stopAllTasks(): class Task(Thread): - def __init__(self, delay=5, daemon = True): - Thread.__init__(self, daemon = daemon) + def __init__(self, delay=5, daemon = True, name='recurring'): + Thread.__init__(self, name=name, daemon = daemon) self.exitFlag = False self.lock = Manager.threadLock self.delay = delay if not self.name in Manager.taskList: Manager.taskList.append(self) + def run(self): self.process() + def process(self): while not self.exitFlag: self.lock.acquire() @@ -49,12 +57,15 @@ def process(self): break time.sleep(0.5) + def task(self): raise RuntimeError("task must be overridden") + def stop(self): self.exitFlag = True + def beforeStop(self): """ Action done when closing thread @@ -62,10 +73,11 @@ def beforeStop(self): if self in Manager.taskList: Manager.taskList.remove(self) + class OneShotTask(Thread): - def __init__(self, daemon = True): - Thread.__init__(self, daemon = daemon) + def __init__(self, daemon = True,name='Oneshot'): + Thread.__init__(self, name=name, daemon = daemon) self.lock = Manager.threadLock if not self.name in Manager.taskList: Manager.taskList.append(self) @@ -87,4 +99,4 @@ def beforeStop(self): Action done when closing thread """ if self in Manager.taskList: - Manager.taskList.remove(self) \ No newline at end of file + Manager.taskList.remove(self) diff --git a/doc/source/BAC0.core.app.rst b/doc/source/BAC0.core.app.rst index b1f7da9c..81c7e8f7 100644 --- a/doc/source/BAC0.core.app.rst +++ b/doc/source/BAC0.core.app.rst @@ -1,22 +1,18 @@ BAC0.core.app package ===================== -Submodules ----------- - -BAC0.core.app.ScriptApplication module --------------------------------------- +BAC0.core.app.ScriptApplication +------------------------------- .. automodule:: BAC0.core.app.ScriptApplication :members: :undoc-members: :show-inheritance: - Module contents --------------- .. automodule:: BAC0.core.app :members: :undoc-members: - :show-inheritance: + :show-inheritance: \ No newline at end of file diff --git a/doc/source/BAC0.core.devices.rst b/doc/source/BAC0.core.devices.rst index 279930f6..5b08a24e 100644 --- a/doc/source/BAC0.core.devices.rst +++ b/doc/source/BAC0.core.devices.rst @@ -1,11 +1,9 @@ BAC0.core.devices package ========================= -Submodules ----------- -BAC0.core.devices.Device module -------------------------------- +BAC0.core.devices.Device +------------------------ .. automodule:: BAC0.core.devices.Device :members: @@ -13,10 +11,10 @@ BAC0.core.devices.Device module :show-inheritance: -Module contents ---------------- +BAC0.core.devices.Points +------------------------ -.. automodule:: BAC0.core.devices +.. automodule:: BAC0.core.devices.Points :members: :undoc-members: :show-inheritance: diff --git a/doc/source/BAC0.core.functions.rst b/doc/source/BAC0.core.functions.rst index bc953e3c..9919d98d 100644 --- a/doc/source/BAC0.core.functions.rst +++ b/doc/source/BAC0.core.functions.rst @@ -1,35 +1,33 @@ BAC0.core.functions package =========================== -Submodules ----------- -BAC0.core.functions.GetIPAddr module ------------------------------------- +BAC0.core.functions.GetIPAddr +----------------------------- .. automodule:: BAC0.core.functions.GetIPAddr :members: :undoc-members: :show-inheritance: -BAC0.core.functions.PrintDebug module -------------------------------------- +BAC0.core.functions.PrintDebug +------------------------------ .. automodule:: BAC0.core.functions.PrintDebug :members: :undoc-members: :show-inheritance: -BAC0.core.functions.WhoisIAm module ------------------------------------ +BAC0.core.functions.WhoisIAm +---------------------------- .. automodule:: BAC0.core.functions.WhoisIAm :members: :undoc-members: :show-inheritance: -BAC0.core.functions.discoverPoints module ------------------------------------------ +BAC0.core.functions.discoverPoints +---------------------------------- .. automodule:: BAC0.core.functions.discoverPoints :members: diff --git a/doc/source/BAC0.core.io.rst b/doc/source/BAC0.core.io.rst index 55a86c2a..d10a4959 100644 --- a/doc/source/BAC0.core.io.rst +++ b/doc/source/BAC0.core.io.rst @@ -1,46 +1,35 @@ BAC0.core.io package ==================== -Submodules ----------- - -BAC0.core.io.IOExceptions module --------------------------------- +BAC0.core.io.IOExceptions +------------------------- .. automodule:: BAC0.core.io.IOExceptions :members: :undoc-members: :show-inheritance: -BAC0.core.io.Read module ------------------------- +BAC0.core.io.Read +----------------- .. automodule:: BAC0.core.io.Read :members: :undoc-members: :show-inheritance: -BAC0.core.io.Simulate module ----------------------------- +BAC0.core.io.Simulate +--------------------- .. automodule:: BAC0.core.io.Simulate :members: :undoc-members: :show-inheritance: -BAC0.core.io.Write module -------------------------- +BAC0.core.io.Write +------------------ .. automodule:: BAC0.core.io.Write :members: :undoc-members: :show-inheritance: - -Module contents ---------------- - -.. automodule:: BAC0.core.io - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/BAC0.rst b/doc/source/BAC0.rst index 35f60bdc..56d25d95 100644 --- a/doc/source/BAC0.rst +++ b/doc/source/BAC0.rst @@ -8,10 +8,15 @@ Subpackages BAC0.core BAC0.scripts + + +.. tbd + BAC0.sql BAC0.tasks BAC0.bokeh + Module contents --------------- diff --git a/doc/source/BAC0.scripts.rst b/doc/source/BAC0.scripts.rst index 35c1f605..f3039e1a 100644 --- a/doc/source/BAC0.scripts.rst +++ b/doc/source/BAC0.scripts.rst @@ -1,30 +1,21 @@ BAC0.scripts package ==================== -Submodules ----------- -BAC0.scripts.BasicScript module -------------------------------- +BAC0.scripts.BasicScript +------------------------ .. automodule:: BAC0.scripts.BasicScript :members: :undoc-members: :show-inheritance: -BAC0.scripts.ReadWriteScript module ------------------------------------ + +BAC0.scripts.ReadWriteScript +---------------------------- .. automodule:: BAC0.scripts.ReadWriteScript :members: :undoc-members: :show-inheritance: - -Module contents ---------------- - -.. automodule:: BAC0.scripts - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/README.rst b/doc/source/README.rst index ad001d0d..e55fb201 100644 --- a/doc/source/README.rst +++ b/doc/source/README.rst @@ -1,41 +1,50 @@ BAC0 |build-status| |coverage| |docs| ===================================== -BAC0 is a Python 3 (3.4 and over) scripting application that uses BACpypes_ to process BACnet messages on a IP network. -This library brings out simple commands to browse a BACnet network, read properties from BACnet devices or write to them. +BAC0 is a Python 3 (3.4 and later) scripting application that uses BACpypes_ to process BACnet messages on a IP network. +This library exposes simple functions to browse the BACnet network, and read & write properties from the BACnet devices. -Python is a simple language to learn and a very powerful tool for data processing. Coupled to BACnet, it becomes a great -tool to test devices an interact with controllers. +Python is a simple language to learn and a very powerful tool for data processing. Coupled with BACnet, +it becomes a **great tool for testing BACnet** and interacting with BACnet controllers. -BAC0 takes its name from the default IP port used by BACnet/IP communication which is port 47808. In hexadecimal, it's written 0xBAC0. +BAC0 takes its name from the default IP port assigned to BACnet/IP communications - port (47808 decimal, 0xBAC0 +hexadecimal). Test driven development (TDD) for DDC controls ============================================== -BAC0 is made for building automation system (BAS) programmers. Controllers used in this field are commonly called DDC Controllers (Direct Digital Control). +BAC0 is intended for assisting BAS (building automation system) programmers, with configuring, testing, and +commissioning of BAS Controllers - often called DDC (Direct Digital Control) Controllers. -Typical controllers can be programmed in different ways, depending on the manufacturer selling them (block programming, basic "kinda" scripts, C code, etc...). -BAC0, is a unified way, using Python language and BACnet/IP communication, to interact with those controllers once their sequence is built. +Typically BAS controllers are programmed using vendor specific tools, and vendor specific programming languages +to define how they will operate. The resulting programs are the controller's **sequence of operations**. +Different vendors, use different methods to define these sequences - including 'block programming', +'graphical programming', and 'text/procedural programming'. -BAC0 allows users to simply test an application even if sensors are not connected to the controller. Using the out_of_service -property, it's easy to write a value to the input so the controller will think an input is conencted. +BAC0 provides a generalized (vendor-independent) means to programmatically interact with the BAS controllers, +via Python and the BACnet/IP communication protocol. BAC0 allows users to test a controller even if no sensors +or outputs are connected to the controller. Thanks to the BACnet **out_of_service** property, it is easy to write +a value to the input pin(s) so the controller believes a sensor is connected, and its **operating sequence** will +respond accordingly. Likewise, it is possible to write a value to an output pin(s) to operate any connected +equipment (often called a **manual command** or to **override an output**). In fact, BAC0 exposes a great many of a +controller's BACnet Objects and Object Properties, enabling automated interactions using Python; as a simple +scripting language, a powerful testing & commissioning tool, or a general application development environment. -It's also possible to do "manual commands" on output (often called overrides). In fact, every variable is exposed and seen by BAC0 and -it's possible to interact with them using a simple scripting language or a complete unit test suite (like Pytest). +Using BAC0 as test tool, makes automated BAS testing quick, reliable, and repeatable. Compare this to +the BAS vendor provided tools, which only allow the controllers to be programmed, and where all the +testing must be done manually. Very slow. Very error-prone. Now you can write your tests and re-run them +as often as you need. -Without a program like BAC0, you can rely on your DDC programming tool... but it is often slow and -every test must be done manually. That means also that if you want to repeat the tests, the more complicated they are, the less chance you'll be able to do so. -Now you can write your test and run them as often as you want. We'll show you how it works. +Better commissioning thanks to automatic data logging +===================================================== +As you will discover, when you define a controller in BAC0, you automatically get **historical data logs** for +every variable in the controller. All I/O points are trended every 10 seconds (by default). Meaning +you can do data analysis of the controller's operation while you're doing your basic **sequence testing**. +This gives you a high-level overview of the controller's performance while highlighting trouble areas really fast. -Better start-up with data acquisition -===================================== -As you will discover, when you define a controller in BAC0, you will get access to historical data of -every variables in the controllers. Every points are trended every 10 seconds by default. Which means -that you can do data analysis on everything while you're doing your startup. It allows to see performances and -trouble really fast. - -This make BAC0 not only a good tool to test your sequence while your in the office. -But also a really good tool to assist your startup, test and balancing. Using Jupyter Notebook, you'll -even be able to create nice looking report right from your code. +BAC0 is not only a good tool for testing your **sequence of operations** while in-the-office. +It is also a really good tool to assist on-site. Use it to test controller startup, operation, and balancing +in-the-field. When combined with Jupyter Notebook, you are even able to create nice looking reports right from your +automation code. .. |build-status| image:: https://travis-ci.org/ChristianTremblay/BAC0.svg?branch=master diff --git a/doc/source/conf.py b/doc/source/conf.py index ec85f4d0..5d0d3792 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -16,6 +16,13 @@ import sys import os import shlex + +# Magic: allowing sphinx to find the 'current source' vs the installed BAC0 library. +PACKAGE_PARENT = '../..' +SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__)))) +sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT))) +print(sys.path) + from BAC0 import infos as infos # If extensions (or modules to document with autodoc) are in another directory, diff --git a/doc/source/connect.rst b/doc/source/connect.rst index e7cc1513..72dd3641 100644 --- a/doc/source/connect.rst +++ b/doc/source/connect.rst @@ -27,39 +27,81 @@ To read a point, simply ask for it using bracket syntax:: mycontroller['point_name'] -Write to a point ----------------- -simple write + +Writing to Points +----------------- + +Simple write ************ -If point is a analogValue, binaryValue or a multistateValue BAC0 will write to the default -priority :: +If point is a value: + + * analogValue (AV) + * binaryValue (BV) + * multistateValue (MV) + +You can change its value with a simple assignment. BAC0 will write the value to the object's +**presentValue** at the default priority.:: + + mycontroller['point_name'] = 23 + +.. image:: images/AV_write.png + + +Write to an Output (Override) +***************************** +If the point is an output: + + * analogOutput (AO) + * binaryOutput (BO) + * multistateOutput (MO) + +You can change its value with a simple assignment. BAC0 will write the value to the object's +**presentValue** (a.k.a override it) at priority 8 (Manual Operator).:: + + mycontroller['outputName'] = 45 + +.. image:: images/AO_write.png + + +Write to an Input (simulate) +**************************** +If the point is an input: + + * analogInput (AI) + * binaryOutput (BO) + * multistateOutput (MO) + +You can change its value with a simple assigment, thus overriding any external value it is +reading and simulating a different sensor reading. The override occurs because +BAC0 sets the point's **out_of_service** (On) and then writes to the point's **presentValue**. + + mycontroller['inputName'] = + + mycontroller['Temperature'] = 23.5 # overiding actual reading of 18.8 C - mycontroller['point_name'] = 10 +.. image:: images/AI_override.png -Relinquish default -****************** -If you must write to relinquish default, it must be said explicitly :: - mycontroller['pointToChange'].default(10) +Releasing an Input simulation or Output override +************************************************* -This distinction is made because of the sensibility to multiple writes to those values. -Thoses are often written to EEPROM directly and have a ±250000 write cycle. +To return control of an Input or Output back to the controller, it needs to be released. +Releasing a point returns it automatic control. This is done with an assignment to 'auto'.:: -Override -********* -If the point is a output, BAC0 will override it (@priority 8):: + mycontroller['pointToRelease'] = 'auto' - mycontroller['outputName'] = 100 +.. image:: images/AI_auto.png +.. image:: images/AO_auto.png -simulate (out_of_service) -************************** -If the point is an input, BAC0 will set the out_of_service flag to On and write -to the present value (which will simulate it):: + +Setting a Relinquish_Default +**************************** +When a point (with a priority array) is released of all override commands, it takes on the value +of its **Relinquish_Default**. [BACnet clause 12.4.12] If you wish to set this default value, +you may with this command:: - mycontroller['inputName'] = 34 + mycontroller['pointToChange'].default() + mycontroller['Output'].default(75) -Releasing a simulation or an override -************************************** -Simply affect 'auto' to the point :: +.. image:: images/AO_set_default.png - mycontroller['pointToRelease'] = 'auto' \ No newline at end of file diff --git a/doc/source/histories.rst b/doc/source/histories.rst index 567252b1..6bb38404 100644 --- a/doc/source/histories.rst +++ b/doc/source/histories.rst @@ -1,34 +1,51 @@ Histories in BAC0 ==================== -As said, every points get saved in a pandas Series every 10 seconds by default. -This means that you can look for historical data from the moment you connect to a device. -Access a historyTable:: + +BAC0 uses the Python Data Analysis library **pandas** [http://pandas.pydata.org/] to +maintain histories of point values over time. All points are saved by BAC0 in a **pandas** +Series every 10 seconds (by default). This means you will automatically have historical data +from the moment you connect to a BACnet device. + +Access the contents of a point's history is very simple.:: - controller['nvoAI1'].history - -Result example :: - - controller['nvoAI1'].history - Out[8]: - 2015-09-20 21:41:37.093985 21.740000 - 2015-09-20 21:42:23.672387 21.790001 - 2015-09-20 21:42:34.358801 21.790001 - 2015-09-20 21:42:45.841596 21.790001 - 2015-09-20 21:42:56.308144 21.790001 - 2015-09-20 21:43:06.897034 21.790001 - 2015-09-20 21:43:17.593321 21.790001 - 2015-09-20 21:43:28.087180 21.790001 - 2015-09-20 21:43:38.597702 21.790001 - 2015-09-20 21:43:48.815317 21.790001 - 2015-09-20 21:44:00.353144 21.790001 - 2015-09-20 21:44:10.871324 21.790001 + controller['pointName'].history + +Example :: + + controller['Temperature'].history + 2017-03-30 12:50:46.514947 19.632507 + 2017-03-30 12:50:56.932325 19.632507 + 2017-03-30 12:51:07.336394 19.632507 + 2017-03-30 12:51:17.705131 19.632507 + 2017-03-30 12:51:28.111724 19.632507 + 2017-03-30 12:51:38.497451 19.632507 + 2017-03-30 12:51:48.874454 19.632507 + 2017-03-30 12:51:59.254916 19.632507 + 2017-03-30 12:52:09.757253 19.536366 + 2017-03-30 12:52:20.204171 19.536366 + 2017-03-30 12:52:30.593838 19.536366 + 2017-03-30 12:52:40.421532 19.536366 + dtype: float64 + + +.. note:: + **pandas** is an extensive data analysis tool, with a vast array of data manipulation operators. + Exploring these is beyond the scope of this documentation. Instead we refer you to this + cheat sheet [https://github.com/pandas-dev/pandas/blob/master/doc/cheatsheet/Pandas_Cheat_Sheet.pdf] and + the pandas website [http://pandas.pydata.org/]. + Resampling data --------------- -As those are pandas DataFrame or Series, you can resample data:: +One common task associated with point histories is preparing it for use with other tools. +This usually involves (as a first step) changing the frequency of the data samples - called +**resampling** in pandas terminology. - # This piece of code show what can of operation can be made using Pandas - +Since the point histories are standard pandas data structures (DataFrames, and Series), you can +manipulate the data with pandas operators, as follows.:: + + # code snipet showing use of pandas operations on a BAC0 point history. + # Resample (consider the mean over a period of 1 min) tempPieces = { '102_ZN-T' : local102['ZN-T'].history.resample('1min'), @@ -44,7 +61,7 @@ As those are pandas DataFrame or Series, you can resample data:: '110_ZN-T' : local110['ZN-T'].history.resample('1min'), '110_ZN-SP' : local110['ZN-SP'].history.resample('1min'), } - # Remove any NaN value + # Remove any NaN values temp_pieces = pd.DataFrame(tempPieces).fillna(method = 'ffill').fillna(method = 'bfill') # Create a new column in the DataFrame which is the error between setpoint and temperature @@ -57,4 +74,4 @@ As those are pandas DataFrame or Series, you can resample data:: # Create a new dataframe from results and show some statistics temp_erreurs = temp_pieces[['Erreur_102', 'Erreur_104', 'Erreur_105', 'Erreur_106', 'Erreur_109', 'Erreur_110']] - temp_erreurs.describe() \ No newline at end of file + temp_erreurs.describe() diff --git a/doc/source/images/AI_auto.png b/doc/source/images/AI_auto.png new file mode 100644 index 00000000..8395cb59 Binary files /dev/null and b/doc/source/images/AI_auto.png differ diff --git a/doc/source/images/AI_override.png b/doc/source/images/AI_override.png new file mode 100644 index 00000000..33bbd582 Binary files /dev/null and b/doc/source/images/AI_override.png differ diff --git a/doc/source/images/AO_auto.png b/doc/source/images/AO_auto.png new file mode 100644 index 00000000..72c4f46b Binary files /dev/null and b/doc/source/images/AO_auto.png differ diff --git a/doc/source/images/AO_set_default.png b/doc/source/images/AO_set_default.png new file mode 100644 index 00000000..9574a4c3 Binary files /dev/null and b/doc/source/images/AO_set_default.png differ diff --git a/doc/source/images/AO_write.png b/doc/source/images/AO_write.png new file mode 100644 index 00000000..ee2688e2 Binary files /dev/null and b/doc/source/images/AO_write.png differ diff --git a/doc/source/images/AV_write.png b/doc/source/images/AV_write.png new file mode 100644 index 00000000..296411d5 Binary files /dev/null and b/doc/source/images/AV_write.png differ diff --git a/doc/source/index.rst b/doc/source/index.rst index 29bc81d0..d611f660 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,8 +3,9 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to BACØ's documentation! -================================ +Welcome to BACØ - BACnet Test Tool +================================== + .. include:: README.rst @@ -22,9 +23,14 @@ Table of contents tests pytest -Modules documentation -===================== -.. include:: BAC0.rst + +Developer documentation +======================= +.. toctree:: + :maxdepth: 2 + + BAC0 + Index and search tool ====================== diff --git a/doc/source/pytest.rst b/doc/source/pytest.rst index 3ae69099..01a875ea 100644 --- a/doc/source/pytest.rst +++ b/doc/source/pytest.rst @@ -1,32 +1,24 @@ Using Pytest_ ============= -Pytest_ is a "a mature full-featured Python testing tool". -It allows the creation of test files that can be called by a command line script. -It's then possible to create different test, all in there own file start -the process and work on something else while tests are running. +Pytest_ [https://docs.pytest.org/en/latest/] is a "a mature full-featured Python testing tool". +It allows the creation of test files that can be called by a command line script, +and run automatically while you work on something else. -Here an example of a Pytest_ module that could be created. +For more details, please refer Pytest's documentation. -Please note that Pytest_ on its own is a very complete solution. For more documentation about -it, please refer to the documentation of the project. - -I'll simply describe minimal feature I present in the example. Some basic stuff before we begin -------------------------------- -Pytest is a very simple testing tool. The default unit test tool for python is called -unittest. Based on Java xUnit, it's more formal, uses a lot of different functions and classes... -It can easily become too much for the needs of testing DDC controllers. - -Pytest uses only simple `assert` command, inside functions defined in a module. +Pytest is a very simple testing tool. While, the default unit test tool for python is +**unittest** (which is more formal and has more features); unittest can easily become +too much for the needs of testing DDC controllers. -Pytest also allows the usage of "fixtures" which are little snippets of code that will be -used to prepare what's needed for the test, then finalize the process when it's over. In unittest, -thoses functions are called setUp and tearDown. +Pytest uses only simple the `assert` command, and locally defined functions. +It also allows the usage of "fixtures" which are little snippets of code that prepare things +prior to the test (setUp), then finalize things when the test is over (tearDown). -In the example module I'll show you, I'm using fixtures to create the BACnet connection at the beginning -of the test, making this connection valid for all the tests in the module, then closing the connection -after the tests have been done, just after having saved the controller so the histories be available. +The following example uses fixtures to establish the BACnet connection prior to the test, +and then saves the controller histories and closes the connection after the tests are done. Example +++++++ @@ -69,12 +61,12 @@ Code :: Success result .............. -If you named you file test_mytest.py, you can just run :: +If you name the file: test_mytest.py, you can just run :: py.test -v -s -Pytest_ will look for test files, find them and run them. Or you can define the -file you want to run :: +Pytest_ will look for the test files, find them and run them. Or you can define the +exact file you want to run :: py.test mytestfile.py -v -s @@ -117,7 +109,7 @@ Here's what it looks like :: Failure result .............. -Here's what it looks like when a test fails :: +Here's what a test failure looks like:: ============================= test session starts ============================= platform win32 -- Python 3.4.4, pytest-2.8.5, py-1.4.31, pluggy-0.3.1 -- C:\User @@ -163,12 +155,12 @@ Here's what it looks like when a test fails :: pytest_example.py:30: AssertionError ===================== 1 failed, 1 passed in 30.71 seconds ===================== -I modified the test here to generate an failure if nvoAI2 is not greater than 1000. +Note: I modified the test to generate an failure - nvoAI2 cannot exceed 1000. Conclusion ---------- Using Pytest_ is a really good way to generate test files that can be reused and modified depending on different use cases. It's a good way to run multiple tests at once. -It will give you a concise report of every failure and tell you if tests succeeded. +It provides concise reports of every failure and tells you when your tests succeed. -.. _pytest : http://pytest.org/latest/ \ No newline at end of file +.. _pytest : http://pytest.org/latest/ diff --git a/doc/source/savedevice.rst b/doc/source/savedevice.rst index a7bb3775..e453acb4 100644 --- a/doc/source/savedevice.rst +++ b/doc/source/savedevice.rst @@ -1,39 +1,40 @@ Saving your data ================ -When doing some tests, it can be useful to go back in time and see what -happened before. BAC0 allow you to save your progress (historical data) to a file -so you'll be able to re-open your device later. +When doing tests, it can be useful to go back in time and see what +happened before. BAC0 allows you to save your progress (historical data) to a file +that you'll be able to re-open in your device later. Use :: controller.save() -and voila ! 2 new files will be created. One sqlite with all the histories, and -one bin file with all the details and properties of the device so it can be +and voila! Two files are created. One (an SQLite file) contains all the histories, and +one binary file containing all the details and properties of the device so the details can be rebuilt when needed. -By default, the 'object name' of the device will be used as the filename. But you can specify a name :: +By default, the 'object name' of the device is used as the filename. But you can specify a name :: controller.save(db='new_name') Offline mode ------------ -As already explained, a device in BAC0, if not connected (cannot be reached) will be -created as an offline device. If a database exist for this device, it will be -created and every points and histories will be available just like if you were +As already explained, a device in BAC0, if not connected (or cannot be reached) will be +created as an offline device. If a database exists for this device, it will automatically +loaded and all the points and histories will be available just as if if you were actually connected to the network. -You can also force a connection to the database if needed. Given a connected device use :: +You can also force a connection to use an existing database if needed. +Provide connect function with the desired database's name.:: controller.connect(db='db_name') -Please note that it's actually an experimental feature. +Please note: this feature is experimental. Saving Data to Excel -------------------- -Using the module called xlwings, it's possible to export all the data of the controller -to an Excel Workbook. +Thought the use of the Python module xlwings [https://www.xlwings.org/], it's possible to export all +the data of a controller into an Excel Workbook. Example :: - controller.to_excel() \ No newline at end of file + controller.to_excel() diff --git a/doc/source/tests.rst b/doc/source/tests.rst index 0d049bdf..d9cb7c06 100644 --- a/doc/source/tests.rst +++ b/doc/source/tests.rst @@ -1,30 +1,35 @@ Testing and simulating with BAC0 ================================ -Now you can build simple tests using assert syntax for example and make your DDC code stronger. + +BAC0 is a powerful BAS test tool. With it you can easily build tests scripts, and by +using its **assert** syntax, you can make your DDC code stronger. + Using Assert and other commands ------------------------------- -Let's say your sequence is really simple. Something like this : +Let's say your BAC controller **sequence of operation** is really simple. Something like this:: -System stopped --------------- -When system is stopped, fan must be off, dampers must be closed, heater cannot operate. + System stopped: + When system is stopped, fan must be off, + dampers must be closed, heater cannot operate. -System started --------------- -When system starts, fan command will be on. Dampers will open to minimum position. -If fan status turns on, heating sequence will starts. + System started: + When system starts, fan command will be on. + Dampers will open to minimum position. + If fan status turns on, heating sequence will start. And so on... How would I test that ? ----------------------- -* Controller is defined and its variable name is mycontroller -* fan command = SF-C -* Fan Status = SF-S -* Dampers command = MAD-O -* Heater = RH-O -* Occupancy command = OCC-SCHEDULE + +Assuming: + * Controller is defined and its variable name is mycontroller + * fan command = SF-C + * Fan Status = SF-S + * Dampers command = MAD-O + * Heater = RH-O + * Occupancy command = OCC-SCHEDULE System Stopped Test Code:: @@ -49,39 +54,44 @@ Sytstem Started Test Code:: And so on... -You are now able to define any test you want. You will probably use more precise conditions -instead of time.sleep() function (example read a value that tells actual mode is active) +You can define any test you want. As complex as you want. You will use more precise conditions +instead of a simple time.sleep() function - most likely you will read a point value that tells +you when the actual mode is active. + +You can then add tests for the various temperature ranges; and build functions to simulate discharge air +temperature depending on the heating or cooling stages... it's all up to you! -You can then test random temperature values, build functions that will simulate discharge air -temperature depending on heatign or cooling stages... it's up to you ! Using tasks to automate simulation ================================== + Polling ------- -Let's say you want to poll a point every 5 seconds to see later how the point reacted.:: +Let's say you want to poll a point every 5 seconds to see how the point reacted.:: mycontroller['point_name'].poll(delay=5) -Note that by default, polling is on for every points every 10 seconds. But you could have -define a controller without polling and do specific polling.:: +Note: by default, polling is enabled on all points at a 10 second frequency. But you could + define a controller without polling and do specific point polling.:: mycontroller = BAC0.device('2:5',5,bacnet,poll=0) mycontroller['point_name'].poll(delay=5) Match ----- -Let's say you want to automatically match the status of a point with the command.:: +Let's say you want to automatically match the status of a point with it's command to +find times when it is reacting to conditions other than what you expected.:: mycontroller['status'].match(mycontroller['command']) + Custom function --------------- -You could also define a complex function, and send it to the controller. +You could also define a complex function, and send that to the controller. This way, you'll be able to continue using all synchronous functions of Jupyter Notebook for example. (technically, a large function will block any inputs until it's finished) -PLEASE NOTE THAT THIS IS A WORK IN PROGRESS +.. note:: THIS IS A WORK IN PROGRESS Example :: @@ -95,6 +105,7 @@ Example :: controller.do(test_Vernier) -This function updates the variable named "Vernier Sim" each 30 seconds. By increment of 1 percent. -It will take a really long time to finish. Using the "do" method, you send the function to the controller -and it will be handled by a thread so you'll be able to continue working on the device. \ No newline at end of file +This function updates the variable named "Vernier Sim" each 30 seconds; incrementing by 1 percent. +This will take a really long time to finish. So instead, use the "do" method, and the function +will be run is a separate thread so you are free to continue working on the device, while the +function commands the controller's point. diff --git a/doc/source/trending.rst b/doc/source/trending.rst index 339eecd7..f1c86741 100644 --- a/doc/source/trending.rst +++ b/doc/source/trending.rst @@ -1,13 +1,16 @@ Trends ====== -Trending is a nice feature when you want to see what's going on. Until now, -it was possible to use matplotlib directly in Jupyter_ to show trends. +Trending is a nice feature when you want to see how a points value changed over time. +Until now, this was only possible using matplotlib directly in Jupyter_. +But I recently became aware of Bokeh_ [http://bokeh.pydata.org/en/latest/] which brings +a complete set of wonderful features for visualizing point histories (a.k.a. trends). +The best feature of all - the ability to see Live Trends of your data as it occurs. Matplotlib ---------- -Matplotlib_ is a well know library for plotting with python. As historical data are -pandas Series or DataFrame, it's possible to use Matplotlib with BAC0. -Show a chart using matplotlib:: +Matplotlib_ is a well known data plotting library for Python. As BAC0's historical point data +are pandas Series and DataFrames, it's possible to use Matplotlib with BAC0. +i.e. Showing a chart using matplotlib:: %matplotlib notebook # or matplotlib inline for a basic interface @@ -15,25 +18,30 @@ Show a chart using matplotlib:: |matplotlib| + Bokeh ----- -But I recently got aware of Bokeh_ which brings a complete new set of wonderful -features to see trends. Best of all, the ability to see Live Trends of your data. +Bokeh is a Python interactive visualization library targeting modern web browsers for presentation. +Its goal is to provide elegant, concise graphics, with high-performance interactivity over very large +or streaming datasets. Bokeh can help anyone who would like to quickly create interactive plots, dashboards, +and data applications. + +BAC0 trending features use Bokeh by default. -Default trending features of BAC0 now depends on Bokeh_ library Bokeh serve ----------- -To be able to use live trending features, a bokeh server needs to run locally on the machine. -When the application starts, a bokeh server will be started in a subprocess. -This server is available on localhost:5006, on your machine. +To use the live trending features, a bokeh server needs to be running locally on your computer. +When the BAC0 starts, it starts a bokeh server for you, running locally. This server is available +at localhost:5006, on your machine. -It's a shortcut so the user don't have to think about starting the server using:: +The server can be started manually, from the command line via:: bokeh serve -Note : Once started, the bokeh server won't be stopped by the BAC0. It will terminate when -Jupyter session will be closed. +Note : Once started, the bokeh server won't be stopped by the BAC0. It will terminate when your +Jupyter session is closed. + Add plots to Bokeh Document --------------------------- @@ -56,22 +64,23 @@ Empty at first, you need to send the data you want to the server using :: |bokeh_plots| -At startup, the script will give you the complete address to reach to get access -to the trends :: +At startup, BAC0 prints the complete URL address for your web browser to view trends :: Click here to open Live Trending Web Page http://localhost:5006/?bokeh-session-id=f9OdQd0LWSPXsnuNdCqVSoEa5xxwd32cZR0ioi9ACXzl + Bokeh Features -------------- -You'll get access to live stream of data and all the features of bokeh (zooming, span, etc.) -For more details, see http://www.bokehplots.com +Bokeh has an extensive set of features. Exploring them is beyond the scope of this documentation. +Instead you may discover them yourself at [http://www.bokehplots.com]. +A couple of its features are highlighted below. -Numerous options are provided by Bokeh plots like a hover tool. +Hover tool: |bokeh_hover| -And a lot of other options like pan, box zoom, mouse wheel zoom, save, etc... +And a lot of other options like pan, box zoom, mouse wheel zoom, save, etc...: |bokeh_tools| diff --git a/tests/first.py b/tests/first.py new file mode 100644 index 00000000..b77c8882 --- /dev/null +++ b/tests/first.py @@ -0,0 +1,40 @@ +''' first.py - explore BAC0 +''' +#--- standard Python modules --- +#--- 3rd party modules --- +#--- this application's modules --- + +#------------------------------------------------------------------------------ + +import BAC0 +import time + +bacnet= BAC0.connect() +time.sleep(3) + +''' +who= bacnet.whois() +print(who) +''' + +d12= BAC0.device('20020:12',1200,bacnet) + +print(d12['Out4']) +d12['Out4'].default(5) +d12['Out4']= 56 +print(d12['Out4']) + +d12['Out4']= 'auto' +print(d12['Out4']) + +print(d12['Temperature']) +d12['Temperature']= 23.5 +print(d12['Temperature']) + +''' +for i in range(4): + val= d12['junk'] # read AV24 +''' + +pass + \ No newline at end of file