Edouard@2020: #!/usr/bin/env python Edouard@2020: # -*- coding: utf-8 -*- Edouard@2020: Edouard@2020: # This file is part of Beremiz, a Integrated Development Environment for Edouard@2020: # programming IEC 61131-3 automates supporting plcopen standard. Edouard@2020: # This files implements the bacnet plugin for Beremiz, adding BACnet server support. Edouard@2020: # Edouard@2020: # Copyright (C) 2017: Mario de Sousa (msousa@fe.up.pt) Edouard@2020: # Edouard@2020: # See COPYING file for copyrights details. Edouard@2020: # Edouard@2020: # This program is free software; you can redistribute it and/or Edouard@2020: # modify it under the terms of the GNU General Public License Edouard@2020: # as published by the Free Software Foundation; either version 2 Edouard@2020: # of the License, or (at your option) any later version. Edouard@2020: # Edouard@2020: # This program is distributed in the hope that it will be useful, Edouard@2020: # but WITHOUT ANY WARRANTY; without even the implied warranty of Edouard@2020: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Edouard@2020: # GNU General Public License for more details. Edouard@2020: # Edouard@2020: # You should have received a copy of the GNU General Public License Edouard@2020: # along with this program; if not, write to the Free Software Edouard@2020: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Edouard@2020: Edouard@2250: from __future__ import absolute_import Edouard@2250: from collections import Counter Edouard@2250: Edouard@2020: import wx Edouard@2250: Edouard@2250: # Import some libraries on Beremiz code Edouard@2020: from util.BitmapLibrary import GetBitmap Edouard@2250: from controls.CustomGrid import CustomGrid Edouard@2250: from controls.CustomTable import CustomTable Edouard@2020: from editors.ConfTreeNodeEditor import ConfTreeNodeEditor Edouard@2250: from graphics.GraphicCommons import ERROR_HIGHLIGHT Edouard@2020: Edouard@2020: Edouard@2020: # BACnet Engineering units taken from: ASHRAE 135-2016, clause/chapter 21 Edouard@2250: BACnetEngineeringUnits = [ Edouard@2250: ('(Acceleration) meters-per-second-per-second (166)', 166), Edouard@2250: ('(Area) square-meters (0)', 0), Edouard@2250: ('(Area) square-centimeters (116)', 116), Edouard@2250: ('(Area) square-feet (1)', 1), Edouard@2250: ('(Area) square-inches (115)', 115), Edouard@2250: ('(Currency) currency1 (105)', 105), Edouard@2250: ('(Currency) currency2 (106)', 106), Edouard@2250: ('(Currency) currency3 (107)', 107), Edouard@2250: ('(Currency) currency4 (108)', 108), Edouard@2250: ('(Currency) currency5 (109)', 109), Edouard@2250: ('(Currency) currency6 (110)', 110), Edouard@2250: ('(Currency) currency7 (111)', 111), Edouard@2250: ('(Currency) currency8 (112)', 112), Edouard@2250: ('(Currency) currency9 (113)', 113), Edouard@2250: ('(Currency) currency10 (114)', 114), Edouard@2250: ('(Electrical) milliamperes (2)', 2), Edouard@2250: ('(Electrical) amperes (3)', 3), Edouard@2250: ('(Electrical) amperes-per-meter (167)', 167), Edouard@2250: ('(Electrical) amperes-per-square-meter (168)', 168), Edouard@2250: ('(Electrical) ampere-square-meters (169)', 169), Edouard@2250: ('(Electrical) decibels (199)', 199), Edouard@2250: ('(Electrical) decibels-millivolt (200)', 200), Edouard@2250: ('(Electrical) decibels-volt (201)', 201), Edouard@2250: ('(Electrical) farads (170)', 170), Edouard@2250: ('(Electrical) henrys (171)', 171), Edouard@2250: ('(Electrical) ohms (4)', 4), Edouard@2250: ('(Electrical) ohm-meter-squared-per-meter (237)', 237), Edouard@2250: ('(Electrical) ohm-meters (172)', 172), Edouard@2250: ('(Electrical) milliohms (145)', 145), Edouard@2250: ('(Electrical) kilohms (122)', 122), Edouard@2250: ('(Electrical) megohms (123)', 123), Edouard@2250: ('(Electrical) microsiemens (190)', 190), Edouard@2250: ('(Electrical) millisiemens (202)', 202), Edouard@2250: ('(Electrical) siemens (173)', 173), Edouard@2250: ('(Electrical) siemens-per-meter (174)', 174), Edouard@2250: ('(Electrical) teslas (175)', 175), Edouard@2250: ('(Electrical) volts (5)', 5), Edouard@2250: ('(Electrical) millivolts (124)', 124), Edouard@2250: ('(Electrical) kilovolts (6)', 6), Edouard@2250: ('(Electrical) megavolts (7)', 7), Edouard@2250: ('(Electrical) volt-amperes (8)', 8), Edouard@2250: ('(Electrical) kilovolt-amperes (9)', 9), Edouard@2250: ('(Electrical) megavolt-amperes (10)', 10), Edouard@2250: ('(Electrical) volt-amperes-reactive (11)', 11), Edouard@2250: ('(Electrical) kilovolt-amperes-reactive (12)', 12), Edouard@2250: ('(Electrical) megavolt-amperes-reactive (13)', 13), Edouard@2250: ('(Electrical) volts-per-degree-kelvin (176)', 176), Edouard@2250: ('(Electrical) volts-per-meter (177)', 177), Edouard@2250: ('(Electrical) degrees-phase (14)', 14), Edouard@2250: ('(Electrical) power-factor (15)', 15), Edouard@2250: ('(Electrical) webers (178)', 178), Edouard@2250: ('(Energy) ampere-seconds (238)', 238), Edouard@2250: ('(Energy) volt-ampere-hours (239)', 239), Edouard@2250: ('(Energy) kilovolt-ampere-hours (240)', 240), Edouard@2250: ('(Energy) megavolt-ampere-hours (241)', 241), Edouard@2250: ('(Energy) volt-ampere-hours-reactive (242)', 242), Edouard@2250: ('(Energy) kilovolt-ampere-hours-reactive (243)', 243), Edouard@2250: ('(Energy) megavolt-ampere-hours-reactive (244)', 244), Edouard@2250: ('(Energy) volt-square-hours (245)', 245), Edouard@2250: ('(Energy) ampere-square-hours (246)', 246), Edouard@2250: ('(Energy) joules (16)', 16), Edouard@2250: ('(Energy) kilojoules (17)', 17), Edouard@2250: ('(Energy) kilojoules-per-kilogram (125)', 125), Edouard@2250: ('(Energy) megajoules (126)', 126), Edouard@2250: ('(Energy) watt-hours (18)', 18), Edouard@2250: ('(Energy) kilowatt-hours (19)', 19), Edouard@2250: ('(Energy) megawatt-hours (146)', 146), Edouard@2250: ('(Energy) watt-hours-reactive (203)', 203), Edouard@2250: ('(Energy) kilowatt-hours-reactive (204)', 204), Edouard@2250: ('(Energy) megawatt-hours-reactive (205)', 205), Edouard@2250: ('(Energy) btus (20)', 20), Edouard@2250: ('(Energy) kilo-btus (147)', 147), Edouard@2250: ('(Energy) mega-btus (148)', 148), Edouard@2250: ('(Energy) therms (21)', 21), Edouard@2250: ('(Energy) ton-hours (22)', 22), Edouard@2250: ('(Enthalpy) joules-per-kilogram-dry-air (23)', 23), Edouard@2250: ('(Enthalpy) kilojoules-per-kilogram-dry-air (149)', 149), Edouard@2250: ('(Enthalpy) megajoules-per-kilogram-dry-air (150)', 150), Edouard@2250: ('(Enthalpy) btus-per-pound-dry-air (24)', 24), Edouard@2250: ('(Enthalpy) btus-per-pound (117)', 117), Edouard@2250: ('(Entropy) joules-per-degree-kelvin (127)', 127), Edouard@2250: ('(Entropy) kilojoules-per-degree-kelvin (151)', 151), Edouard@2250: ('(Entropy) megajoules-per-degree-kelvin (152)', 152), Edouard@2250: ('(Entropy) joules-per-kilogram-degree-kelvin (128)', 128), Edouard@2250: ('(Force) newton (153)', 153), Edouard@2250: ('(Frequency) cycles-per-hour (25)', 25), Edouard@2250: ('(Frequency) cycles-per-minute (26)', 26), Edouard@2250: ('(Frequency) hertz (27)', 27), Edouard@2250: ('(Frequency) kilohertz (129)', 129), Edouard@2250: ('(Frequency) megahertz (130)', 130), Edouard@2250: ('(Frequency) per-hour (131)', 131), Edouard@2250: ('(Humidity) grams-of-water-per-kilogram-dry-air (28)', 28), Edouard@2250: ('(Humidity) percent-relative-humidity (29)', 29), Edouard@2250: ('(Length) micrometers (194)', 194), Edouard@2250: ('(Length) millimeters (30)', 30), Edouard@2250: ('(Length) centimeters (118)', 118), Edouard@2250: ('(Length) kilometers (193)', 193), Edouard@2250: ('(Length) meters (31)', 31), Edouard@2250: ('(Length) inches (32)', 32), Edouard@2250: ('(Length) feet (33)', 33), Edouard@2250: ('(Light) candelas (179)', 179), Edouard@2250: ('(Light) candelas-per-square-meter (180)', 180), Edouard@2250: ('(Light) watts-per-square-foot (34)', 34), Edouard@2250: ('(Light) watts-per-square-meter (35)', 35), Edouard@2250: ('(Light) lumens (36)', 36), Edouard@2250: ('(Light) luxes (37)', 37), Edouard@2250: ('(Light) foot-candles (38)', 38), Edouard@2250: ('(Mass) milligrams (196)', 196), Edouard@2250: ('(Mass) grams (195)', 195), Edouard@2250: ('(Mass) kilograms (39)', 39), Edouard@2250: ('(Mass) pounds-mass (40)', 40), Edouard@2250: ('(Mass) tons (41)', 41), Edouard@2250: ('(Mass Flow) grams-per-second (154)', 154), Edouard@2250: ('(Mass Flow) grams-per-minute (155)', 155), Edouard@2250: ('(Mass Flow) kilograms-per-second (42)', 42), Edouard@2250: ('(Mass Flow) kilograms-per-minute (43)', 43), Edouard@2250: ('(Mass Flow) kilograms-per-hour (44)', 44), Edouard@2250: ('(Mass Flow) pounds-mass-per-second (119)', 119), Edouard@2250: ('(Mass Flow) pounds-mass-per-minute (45)', 45), Edouard@2250: ('(Mass Flow) pounds-mass-per-hour (46)', 46), Edouard@2250: ('(Mass Flow) tons-per-hour (156)', 156), Edouard@2250: ('(Power) milliwatts (132)', 132), Edouard@2250: ('(Power) watts (47)', 47), Edouard@2250: ('(Power) kilowatts (48)', 48), Edouard@2250: ('(Power) megawatts (49)', 49), Edouard@2250: ('(Power) btus-per-hour (50)', 50), Edouard@2250: ('(Power) kilo-btus-per-hour (157)', 157), Edouard@2250: ('(Power) joule-per-hours (247)', 247), Edouard@2250: ('(Power) horsepower (51)', 51), Edouard@2250: ('(Power) tons-refrigeration (52)', 52), Edouard@2250: ('(Pressure) pascals (53)', 53), Edouard@2250: ('(Pressure) hectopascals (133)', 133), Edouard@2250: ('(Pressure) kilopascals (54)', 54), Edouard@2250: ('(Pressure) millibars (134)', 134), Edouard@2250: ('(Pressure) bars (55)', 55), Edouard@2250: ('(Pressure) pounds-force-per-square-inch (56)', 56), Edouard@2250: ('(Pressure) millimeters-of-water (206)', 206), Edouard@2250: ('(Pressure) centimeters-of-water (57)', 57), Edouard@2250: ('(Pressure) inches-of-water (58)', 58), Edouard@2250: ('(Pressure) millimeters-of-mercury (59)', 59), Edouard@2250: ('(Pressure) centimeters-of-mercury (60)', 60), Edouard@2250: ('(Pressure) inches-of-mercury (61)', 61), Edouard@2250: ('(Temperature) degrees-celsius (62)', 62), Edouard@2250: ('(Temperature) degrees-kelvin (63)', 63), Edouard@2250: ('(Temperature) degrees-kelvin-per-hour (181)', 181), Edouard@2250: ('(Temperature) degrees-kelvin-per-minute (182)', 182), Edouard@2250: ('(Temperature) degrees-fahrenheit (64)', 64), Edouard@2250: ('(Temperature) degree-days-celsius (65)', 65), Edouard@2250: ('(Temperature) degree-days-fahrenheit (66)', 66), Edouard@2250: ('(Temperature) delta-degrees-fahrenheit (120)', 120), Edouard@2250: ('(Temperature) delta-degrees-kelvin (121)', 121), Edouard@2250: ('(Time) years (67)', 67), Edouard@2250: ('(Time) months (68)', 68), Edouard@2250: ('(Time) weeks (69)', 69), Edouard@2250: ('(Time) days (70)', 70), Edouard@2250: ('(Time) hours (71)', 71), Edouard@2250: ('(Time) minutes (72)', 72), Edouard@2250: ('(Time) seconds (73)', 73), Edouard@2250: ('(Time) hundredths-seconds (158)', 158), Edouard@2250: ('(Time) milliseconds (159)', 159), Edouard@2250: ('(Torque) newton-meters (160)', 160), Edouard@2250: ('(Velocity) millimeters-per-second (161)', 161), Edouard@2250: ('(Velocity) millimeters-per-minute (162)', 162), Edouard@2250: ('(Velocity) meters-per-second (74)', 74), Edouard@2250: ('(Velocity) meters-per-minute (163)', 163), Edouard@2250: ('(Velocity) meters-per-hour (164)', 164), Edouard@2250: ('(Velocity) kilometers-per-hour (75)', 75), Edouard@2250: ('(Velocity) feet-per-second (76)', 76), Edouard@2250: ('(Velocity) feet-per-minute (77)', 77), Edouard@2250: ('(Velocity) miles-per-hour (78)', 78), Edouard@2250: ('(Volume) cubic-feet (79)', 79), Edouard@2250: ('(Volume) cubic-meters (80)', 80), Edouard@2250: ('(Volume) imperial-gallons (81)', 81), Edouard@2250: ('(Volume) milliliters (197)', 197), Edouard@2250: ('(Volume) liters (82)', 82), Edouard@2250: ('(Volume) us-gallons (83)', 83), Edouard@2250: ('(Volumetric Flow) cubic-feet-per-second (142)', 142), Edouard@2250: ('(Volumetric Flow) cubic-feet-per-minute (84)', 84), Edouard@2250: ('(Volumetric Flow) million-standard-cubic-feet-per-minute (254)', 254), Edouard@2250: ('(Volumetric Flow) cubic-feet-per-hour (191)', 191), Edouard@2250: ('(Volumetric Flow) cubic-feet-per-day (248)', 248), Edouard@2250: ('(Volumetric Flow) standard-cubic-feet-per-day (47808)', 47808), Edouard@2250: ('(Volumetric Flow) million-standard-cubic-feet-per-day (47809)', 47809), Edouard@2250: ('(Volumetric Flow) thousand-cubic-feet-per-day (47810)', 47810), Edouard@2250: ('(Volumetric Flow) thousand-standard-cubic-feet-per-day (47811)', 47811), Edouard@2250: ('(Volumetric Flow) pounds-mass-per-day (47812)', 47812), Edouard@2250: ('(Volumetric Flow) cubic-meters-per-second (85)', 85), Edouard@2250: ('(Volumetric Flow) cubic-meters-per-minute (165)', 165), Edouard@2250: ('(Volumetric Flow) cubic-meters-per-hour (135)', 135), Edouard@2250: ('(Volumetric Flow) cubic-meters-per-day (249)', 249), Edouard@2250: ('(Volumetric Flow) imperial-gallons-per-minute (86)', 86), Edouard@2250: ('(Volumetric Flow) milliliters-per-second (198)', 198), Edouard@2250: ('(Volumetric Flow) liters-per-second (87)', 87), Edouard@2250: ('(Volumetric Flow) liters-per-minute (88)', 88), Edouard@2250: ('(Volumetric Flow) liters-per-hour (136)', 136), Edouard@2250: ('(Volumetric Flow) us-gallons-per-minute (89)', 89), Edouard@2250: ('(Volumetric Flow) us-gallons-per-hour (192)', 192), Edouard@2250: ('(Other) degrees-angular (90)', 90), Edouard@2250: ('(Other) degrees-celsius-per-hour (91)', 91), Edouard@2250: ('(Other) degrees-celsius-per-minute (92)', 92), Edouard@2250: ('(Other) degrees-fahrenheit-per-hour (93)', 93), Edouard@2250: ('(Other) degrees-fahrenheit-per-minute (94)', 94), Edouard@2250: ('(Other) joule-seconds (183)', 183), Edouard@2250: ('(Other) kilograms-per-cubic-meter (186)', 186), Edouard@2250: ('(Other) kilowatt-hours-per-square-meter (137)', 137), Edouard@2250: ('(Other) kilowatt-hours-per-square-foot (138)', 138), Edouard@2250: ('(Other) watt-hours-per-cubic-meter (250)', 250), Edouard@2250: ('(Other) joules-per-cubic-meter (251)', 251), Edouard@2250: ('(Other) megajoules-per-square-meter (139)', 139), Edouard@2250: ('(Other) megajoules-per-square-foot (140)', 140), Edouard@2250: ('(Other) mole-percent (252)', 252), Edouard@2250: ('(Other) no-units (95)', 95), Edouard@2250: ('(Other) newton-seconds (187)', 187), Edouard@2250: ('(Other) newtons-per-meter (188)', 188), Edouard@2250: ('(Other) parts-per-million (96)', 96), Edouard@2250: ('(Other) parts-per-billion (97)', 97), Edouard@2250: ('(Other) pascal-seconds (253)', 253), Edouard@2250: ('(Other) percent (98)', 98), Edouard@2250: ('(Other) percent-obscuration-per-foot (143)', 143), Edouard@2250: ('(Other) percent-obscuration-per-meter (144)', 144), Edouard@2250: ('(Other) percent-per-second (99)', 99), Edouard@2250: ('(Other) per-minute (100)', 100), Edouard@2250: ('(Other) per-second (101)', 101), Edouard@2250: ('(Other) psi-per-degree-fahrenheit (102)', 102), Edouard@2250: ('(Other) radians (103)', 103), Edouard@2250: ('(Other) radians-per-second (184)', 184), Edouard@2250: ('(Other) revolutions-per-minute (104)', 104), Edouard@2250: ('(Other) square-meters-per-newton (185)', 185), Edouard@2250: ('(Other) watts-per-meter-per-degree-kelvin (189)', 189), Edouard@2250: ('(Other) watts-per-square-meter-degree-kelvin (141)', 141), Edouard@2250: ('(Other) per-mille (207)', 207), Edouard@2250: ('(Other) grams-per-gram (208)', 208), Edouard@2250: ('(Other) kilograms-per-kilogram (209)', 209), Edouard@2250: ('(Other) grams-per-kilogram (210)', 210), Edouard@2250: ('(Other) milligrams-per-gram (211)', 211), Edouard@2250: ('(Other) milligrams-per-kilogram (212)', 212), Edouard@2250: ('(Other) grams-per-milliliter (213)', 213), Edouard@2250: ('(Other) grams-per-liter (214)', 214), Edouard@2250: ('(Other) milligrams-per-liter (215)', 215), Edouard@2250: ('(Other) micrograms-per-liter (216)', 216), Edouard@2250: ('(Other) grams-per-cubic-meter (217)', 217), Edouard@2250: ('(Other) milligrams-per-cubic-meter (218)', 218), Edouard@2250: ('(Other) micrograms-per-cubic-meter (219)', 219), Edouard@2250: ('(Other) nanograms-per-cubic-meter (220)', 220), Edouard@2250: ('(Other) grams-per-cubic-centimeter (221)', 221), Edouard@2250: ('(Other) becquerels (222)', 222), Edouard@2250: ('(Other) kilobecquerels (223)', 223), Edouard@2250: ('(Other) megabecquerels (224)', 224), Edouard@2250: ('(Other) gray (225)', 225), Edouard@2250: ('(Other) milligray (226)', 226), Edouard@2250: ('(Other) microgray (227)', 227), Edouard@2250: ('(Other) sieverts (228)', 228), Edouard@2250: ('(Other) millisieverts (229)', 229), Edouard@2250: ('(Other) microsieverts (230)', 230), Edouard@2250: ('(Other) microsieverts-per-hour (231)', 231), Edouard@2250: ('(Other) millirems (47814)', 47814), Edouard@2250: ('(Other) millirems-per-hour (47815)', 47815), Edouard@2250: ('(Other) decibels-a (232)', 232), Edouard@2250: ('(Other) nephelometric-turbidity-unit (233)', 233), Edouard@2250: ('(Other) pH (234)', 234), Edouard@2250: ('(Other) grams-per-square-meter (235)', 235), Edouard@2250: ('(Other) minutes-per-degree-kelvin (236)', 236) Edouard@2250: ] # BACnetEngineeringUnits Edouard@2250: Edouard@2250: Edouard@2020: # ObjectID (22 bits ID + 10 bits type) => max = 2^22-1 = 4194303 Edouard@2020: # However, ObjectID 4194303 is not allowed! Edouard@2250: # 4194303 is used as a special value when object Id reference is referencing an undefined object Edouard@2020: # (similar to NULL in C) Edouard@2020: BACnetObjectID_MAX = 4194302 Edouard@2020: BACnetObjectID_NUL = 4194303 Edouard@2020: Edouard@2020: Edouard@2020: # A base class Edouard@2020: # what would be a purely virtual class in C++ Edouard@2250: class ObjectProperties(object): Edouard@2020: # this __init_() function is currently not beeing used! Edouard@2250: Edouard@2020: def __init__(self): Edouard@2250: # nothing to do Edouard@2020: return Edouard@2020: Edouard@2020: Edouard@2020: class BinaryObject(ObjectProperties): Edouard@2250: # 'PropertyNames' will be used as the header for each column of the Object Properties grid! Edouard@2250: # Warning: The rest of the code depends on the existance of an "Object Identifier" and "Object Name" Edouard@2250: # Be sure to use these exact names for these BACnet object properties! Edouard@2250: PropertyNames = ["Object Identifier", "Object Name", "Description"] Edouard@2250: ColumnAlignments = [wx.ALIGN_RIGHT, wx.ALIGN_LEFT, wx.ALIGN_LEFT] Edouard@2250: ColumnSizes = [40, 80, 80] Edouard@2250: PropertyConfig = { Edouard@2250: "Object Identifier": {"GridCellEditor": wx.grid.GridCellNumberEditor, Edouard@2250: "GridCellRenderer": wx.grid.GridCellNumberRenderer, Edouard@2250: # syntax for GridCellNumberEditor -> "min,max" Edouard@2250: # ObjectID (22 bits ID + 10 bits type) => max = 2^22-1 Edouard@2250: "GridCellEditorParam": "0,4194302"}, Edouard@2250: "Object Name": {"GridCellEditor": wx.grid.GridCellTextEditor, Edouard@2250: "GridCellRenderer": wx.grid.GridCellStringRenderer}, Edouard@2250: "Description": {"GridCellEditor": wx.grid.GridCellTextEditor, Edouard@2250: "GridCellRenderer": wx.grid.GridCellStringRenderer} Edouard@2250: } Edouard@2250: Edouard@2250: Edouard@2020: class AnalogObject(ObjectProperties): Edouard@2250: # 'PropertyNames' will be used as the header for each column of the Object Properties grid! Edouard@2250: # Warning: The rest of the code depends on the existance of an "Object Identifier" and "Object Name" Edouard@2020: # Be sure to use these exact names for these BACnet object properties! Edouard@2020: # Edouard@2020: # NOTE: Although it is not listed here (so it does not show up in the GUI, this object will also Edouard@2020: # keep another entry for a virtual property named "Unit ID". This virtual property Edouard@2020: # will store the ID corresponding to the "Engineering Units" currently chosen. Edouard@2020: # This virtual property is kept synchronised to the "Engineering Units" property Edouard@2020: # by the function PropertyChanged() which should be called by the OnCellChange event handler. Edouard@2250: PropertyNames = ["Object Identifier", "Object Name", Edouard@2250: "Description", "Engineering Units"] # 'Unit ID' Edouard@2250: ColumnAlignments = [ Edouard@2250: wx.ALIGN_RIGHT, wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_LEFT] Edouard@2250: ColumnSizes = [40, 80, 80, 200] Edouard@2250: PropertyConfig = { Edouard@2250: "Object Identifier": {"GridCellEditor": wx.grid.GridCellNumberEditor, Edouard@2250: "GridCellRenderer": wx.grid.GridCellNumberRenderer, Edouard@2250: "GridCellEditorParam": "0,4194302"}, Edouard@2250: "Object Name": {"GridCellEditor": wx.grid.GridCellTextEditor, Edouard@2250: "GridCellRenderer": wx.grid.GridCellStringRenderer}, Edouard@2250: "Description": {"GridCellEditor": wx.grid.GridCellTextEditor, Edouard@2250: "GridCellRenderer": wx.grid.GridCellStringRenderer}, Edouard@2250: "Engineering Units": {"GridCellEditor": wx.grid.GridCellChoiceEditor, Edouard@2250: # use string renderer with choice editor! Edouard@2250: "GridCellRenderer": wx.grid.GridCellStringRenderer, Edouard@2250: # syntax for GridCellChoiceEditor -> comma separated values Edouard@2250: "GridCellEditorParam": ','.join([x[0] for x in BACnetEngineeringUnits])} Edouard@2250: } Edouard@2250: Edouard@2250: # obj_properties should be a dictionary, with keys "Object Identifier", Edouard@2250: # "Object Name", "Description", ... Edouard@2020: def UpdateVirtualProperties(self, obj_properties): Edouard@2250: obj_properties["Unit ID"] = [x[1] Edouard@2250: for x in BACnetEngineeringUnits if x[0] == obj_properties["Engineering Units"]][0] Edouard@2250: Edouard@2020: Edouard@2020: class MultiSObject(ObjectProperties): Edouard@2250: # 'PropertyNames' will be used as the header for each column of the Object Properties grid! Edouard@2250: # Warning: The rest of the code depends on the existance of an "Object Identifier" and "Object Name" Edouard@2250: # Be sure to use these exact names for these BACnet object properties! Edouard@2250: PropertyNames = [ Edouard@2250: "Object Identifier", "Object Name", "Description", "Number of States"] Edouard@2250: ColumnAlignments = [ Edouard@2250: wx.ALIGN_RIGHT, wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_CENTER] Edouard@2250: ColumnSizes = [40, 80, 80, 120] Edouard@2250: PropertyConfig = { Edouard@2250: "Object Identifier": {"GridCellEditor": wx.grid.GridCellNumberEditor, Edouard@2250: "GridCellRenderer": wx.grid.GridCellNumberRenderer, Edouard@2250: "GridCellEditorParam": "0,4194302"}, Edouard@2250: "Object Name": {"GridCellEditor": wx.grid.GridCellTextEditor, Edouard@2250: "GridCellRenderer": wx.grid.GridCellStringRenderer}, Edouard@2250: "Description": {"GridCellEditor": wx.grid.GridCellTextEditor, Edouard@2250: "GridCellRenderer": wx.grid.GridCellStringRenderer}, Edouard@2250: # MultiState Values are encoded in unsigned integer Edouard@2250: # (in BACnet => uint8_t), and can not be 0. Edouard@2250: # See ASHRAE 135-2016, section 12.20.4 Edouard@2250: "Number of States": {"GridCellEditor": wx.grid.GridCellNumberEditor, Edouard@2250: "GridCellRenderer": wx.grid.GridCellNumberRenderer, Edouard@2250: # syntax for GridCellNumberEditor -> "min,max" Edouard@2250: "GridCellEditorParam": "1,255"} Edouard@2250: } Edouard@2020: Edouard@2020: Edouard@2020: # The default values to use for each BACnet object type Edouard@2020: # Edouard@2020: # Note that the 'internal plugin parameters' get stored in the data table, but Edouard@2250: # are not visible in the GUI. They are used to generate the Edouard@2020: # EDE files as well as the C code Edouard@2020: class BVObject(BinaryObject): Edouard@2250: DefaultValues = {"Object Identifier": "", Edouard@2250: "Object Name": "Binary Value", Edouard@2250: "Description": "", Edouard@2250: # internal plugin parameters... Edouard@2250: "BACnetObjTypeID": 5, Edouard@2250: "Ctype": "uint8_t", Edouard@2250: "Settable": "Y"} Edouard@2250: Edouard@2020: Edouard@2020: class BOObject(BinaryObject): Edouard@2250: DefaultValues = {"Object Identifier": "", Edouard@2250: "Object Name": "Binary Output", Edouard@2250: "Description": "", Edouard@2250: # internal plugin parameters... Edouard@2250: "BACnetObjTypeID": 4, Edouard@2250: "Ctype": "uint8_t", Edouard@2250: "Settable": "Y"} Edouard@2250: Edouard@2020: Edouard@2020: class BIObject(BinaryObject): Edouard@2250: DefaultValues = {"Object Identifier": "", Edouard@2250: "Object Name": "Binary Input", Edouard@2250: "Description": "", Edouard@2250: # internal plugin parameters... Edouard@2250: "BACnetObjTypeID": 3, Edouard@2250: "Ctype": "uint8_t", Edouard@2250: "Settable": "N"} Edouard@2250: Edouard@2020: Edouard@2020: class AVObject(AnalogObject): Edouard@2250: DefaultValues = {"Object Identifier": "", Edouard@2250: "Object Name": "Analog Value", Edouard@2250: "Description": "", Edouard@2250: "Engineering Units": '(Other) no-units (95)', Edouard@2250: # internal plugin parameters... Edouard@2250: "Unit ID": 95, # the ID of the engineering unit Edouard@2250: # will get updated by Edouard@2250: # UpdateVirtualProperties() Edouard@2250: "BACnetObjTypeID": 2, Edouard@2250: "Ctype": "float", Edouard@2250: "Settable": "Y"} Edouard@2250: Edouard@2020: Edouard@2020: class AOObject(AnalogObject): Edouard@2250: DefaultValues = {"Object Identifier": "", Edouard@2250: "Object Name": "Analog Output", Edouard@2250: "Description": "", Edouard@2250: "Engineering Units": '(Other) no-units (95)', Edouard@2250: # internal plugin parameters... Edouard@2250: "Unit ID": 95, # the ID of the engineering unit Edouard@2250: # will get updated by Edouard@2250: # UpdateVirtualProperties() Edouard@2250: "BACnetObjTypeID": 1, Edouard@2250: "Ctype": "float", Edouard@2250: "Settable": "Y"} Edouard@2250: Edouard@2020: Edouard@2020: class AIObject(AnalogObject): Edouard@2250: DefaultValues = {"Object Identifier": "", Edouard@2250: "Object Name": "Analog Input", Edouard@2250: "Description": "", Edouard@2250: "Engineering Units": '(Other) no-units (95)', Edouard@2250: # internal plugin parameters... Edouard@2250: "Unit ID": 95, # the ID of the engineering unit Edouard@2250: # will get updated by Edouard@2250: # UpdateVirtualProperties() Edouard@2250: "BACnetObjTypeID": 0, Edouard@2250: "Ctype": "float", Edouard@2250: "Settable": "N"} Edouard@2250: Edouard@2020: Edouard@2020: class MSVObject(MultiSObject): Edouard@2250: DefaultValues = {"Object Identifier": "", Edouard@2250: "Object Name": "Multi-state Value", Edouard@2250: "Description": "", Edouard@2250: "Number of States": "255", Edouard@2250: # internal plugin parameters... Edouard@2250: "BACnetObjTypeID": 19, Edouard@2250: "Ctype": "uint8_t", Edouard@2250: "Settable": "Y"} Edouard@2250: Edouard@2020: Edouard@2020: class MSOObject(MultiSObject): Edouard@2250: DefaultValues = {"Object Identifier": "", Edouard@2250: "Object Name": "Multi-state Output", Edouard@2250: "Description": "", Edouard@2250: "Number of States": "255", Edouard@2250: # internal plugin parameters... Edouard@2250: "BACnetObjTypeID": 14, Edouard@2250: "Ctype": "uint8_t", Edouard@2250: "Settable": "Y"} Edouard@2250: Edouard@2020: Edouard@2020: class MSIObject(MultiSObject): Edouard@2250: DefaultValues = {"Object Identifier": "", Edouard@2250: "Object Name": "Multi-state Input", Edouard@2250: "Description": "", Edouard@2250: "Number of States": "255", Edouard@2250: # internal plugin parameters... Edouard@2250: "BACnetObjTypeID": 13, Edouard@2250: "Ctype": "uint8_t", Edouard@2250: "Settable": "N"} Edouard@2020: Edouard@2020: Edouard@2020: class ObjectTable(CustomTable): Edouard@2020: # A custom wx.grid.PyGridTableBase using user supplied data Edouard@2250: # Edouard@2250: # This will basically store a list of BACnet objects that the slave will support/implement. Edouard@2250: # There will be one instance of this ObjectTable class for each BACnet object type Edouard@2020: # (e.g. Binary Value, Analog Input, Multi State Output, ...) Edouard@2250: # Edouard@2020: # The list of BACnet objects will actually be stored within the self.data variable Edouard@2020: # (declared in CustomTable). Self.data will be a list of dictionaries (one entry per BACnet Edouard@2020: # object). All of these dictionaries in the self.data list will have entries whose keys actually Edouard@2250: # depend on the BACnet type object being handled. The keys in the dictionary will be Edouard@2020: # the entries in the PropertyNames list of one of the following classes: Edouard@2020: # (BVObject, BOObject, BIObject, AVObject, AOObject, AIObject, MSVObject, MSOObject, MSIObject). Edouard@2020: # Edouard@2250: # For example, when handling Binary Value BACnet objects, Edouard@2020: # self.data will be a list of dictionaries (one entry per row) Edouard@2020: # self.data[n] will be a dictionary, with keys "Object Identifier", "Object Name", "Description" Edouard@2250: # for example: self.data[n] = {"Object Identifier":33, "Object Name":"room1", "Description":"xx"} Edouard@2250: # Edouard@2020: # Note that this ObjectTable class merely stores the configuration data. Edouard@2250: # It does not control the display nor the editing of this data. Edouard@2020: # This data is typically displayed within a grid, that is controlled by the ObjectGrid class. Edouard@2020: # Edouard@2250: Edouard@2020: def __init__(self, parent, data, BACnetObjectType): Edouard@2020: # parent : the _BacnetSlavePlug object that is instantiating this ObjectTable Edouard@2020: # data : a list with the data to be shown on the grid Edouard@2020: # (i.e., a list containing the BACnet object properties) Edouard@2020: # Instantiated in _BacnetSlavePlug Edouard@2020: # BACnetObjectType: one of BinaryObject, AnalogObject, MultiSObject Edouard@2020: # (or a class that derives from them). Edouard@2020: # This is actually the class itself, and not a variable!!! Edouard@2020: # However, self.BACnetObjectType will be an instance Edouard@2020: # of said class as we later need to call methods from this class. Edouard@2020: # (in particular, the UpdateVirtualProperties() method) Edouard@2020: # Edouard@2020: # The base class must be initialized *first* Edouard@2250: CustomTable.__init__( Edouard@2250: self, parent, data, BACnetObjectType.PropertyNames) Edouard@2020: self.BACnetObjectType = BACnetObjectType() Edouard@2250: self.ChangesToSave = False Edouard@2250: Edouard@2250: # def _GetRowEdit(self, row): Edouard@2250: # row_edit = self.GetValueByName(row, "Edit") Edouard@2250: # var_type = self.Parent.GetTagName() Edouard@2250: # bodytype = self.Parent.Controler.GetEditedElementBodyType(var_type) Edouard@2250: # if bodytype in ["ST", "IL"]: Edouard@2250: # row_edit = True; Edouard@2250: # return row_edit Edouard@2020: Edouard@2020: def _updateColAttrs(self, grid): Edouard@2020: # wx.grid.Grid -> update the column attributes to add the Edouard@2020: # appropriate renderer given the column name. Edouard@2250: # Edouard@2020: # Otherwise default to the default renderer. Edouard@2250: # print "ObjectTable._updateColAttrs() called!!!" Edouard@2020: for row in range(self.GetNumberRows()): Edouard@2020: for col in range(self.GetNumberCols()): Edouard@2250: PropertyName = self.BACnetObjectType.PropertyNames[col] Edouard@2020: PropertyConfig = self.BACnetObjectType.PropertyConfig[PropertyName] Edouard@2250: grid.SetReadOnly(row, col, False) Edouard@2250: grid.SetCellEditor(row, col, PropertyConfig["GridCellEditor"]()) Edouard@2250: grid.SetCellRenderer(row, col, PropertyConfig["GridCellRenderer"]()) Edouard@2020: grid.SetCellBackgroundColour(row, col, wx.WHITE) Edouard@2250: grid.SetCellTextColour(row, col, wx.BLACK) Edouard@2020: if "GridCellEditorParam" in PropertyConfig: Edouard@2250: grid.GetCellEditor(row, col).SetParameters( Edouard@2250: PropertyConfig["GridCellEditorParam"]) Edouard@2020: self.ResizeRow(grid, row) Edouard@2020: Edouard@2020: def FindValueByName(self, PropertyName, PropertyValue): Edouard@2020: # find the row whose property named PropertyName has the value PropertyValue Edouard@2020: # Returns: row number Edouard@2020: # for example, find the row where PropertyName "Object Identifier" has value 1002 Edouard@2020: # FindValueByName("Object Identifier", 1002). Edouard@2020: for row in range(self.GetNumberRows()): Edouard@2020: if int(self.GetValueByName(row, PropertyName)) == PropertyValue: Edouard@2020: return row Edouard@2020: return None Edouard@2250: Edouard@2250: # Return a list containing all the values under the column named 'colname' Edouard@2020: def GetAllValuesByName(self, colname): Edouard@2020: values = [] Edouard@2020: for row in range(self.GetNumberRows()): Edouard@2020: values.append(self.data[row].get(colname)) Edouard@2020: return values Edouard@2020: Edouard@2020: # Returns a dictionary with: Edouard@2020: # keys: IDs of BACnet objects Edouard@2250: # value: number of BACnet objects using this same Id Edouard@2020: # (values larger than 1 indicates an error as BACnet requires unique Edouard@2020: # object IDs for objects of the same type) Edouard@2020: def GetObjectIDCount(self): Edouard@2250: # The dictionary is built by first creating a list containing the IDs Edouard@2250: # of all BACnet objects Edouard@2020: ObjectIDs = self.GetAllValuesByName("Object Identifier") Edouard@2250: # list of integers instead of strings... Edouard@2250: ObjectIDs_as_int = [int(x) for x in ObjectIDs] Edouard@2020: # This list is then transformed into a collections.Counter class Edouard@2020: # Which is then transformed into a dictionary using dict() Edouard@2020: return dict(Counter(ObjectIDs_as_int)) Edouard@2020: Edouard@2020: # Check whether any object ID is used more than once (not valid in BACnet) Edouard@2020: # (returns True or False) Edouard@2020: def HasDuplicateObjectIDs(self): Edouard@2020: ObjectIDsCount = self.GetObjectIDCount() Edouard@2020: for ObjName in ObjectIDsCount: Edouard@2020: if ObjectIDsCount[ObjName] > 1: Edouard@2020: return True Edouard@2020: return False Edouard@2020: Edouard@2020: # Update the virtual properties of the objects of the classes derived from ObjectProperties Edouard@2020: # (currently only the AnalogObject class had virtua properties, i.e. a property Edouard@2020: # that is determined/calculated based on the other properties) Edouard@2020: def UpdateAllVirtualProperties(self): Edouard@2020: if hasattr(self.BACnetObjectType, 'UpdateVirtualProperties'): Edouard@2020: for ObjProp in self.data: Edouard@2020: self.BACnetObjectType.UpdateVirtualProperties(ObjProp) Edouard@2020: Edouard@2020: Edouard@2020: class ObjectGrid(CustomGrid): Edouard@2020: # A custom wx.grid.Grid (CustomGrid derives from wx.grid.Grid) Edouard@2250: # Edouard@2250: # Needed mostly to customise the initial values of newly added rows, and to Edouard@2020: # validate if the inserted data follows BACnet rules. Edouard@2020: # Edouard@2020: # Edouard@2020: # This ObjectGrid class: Edouard@2020: # Creates and controls the GUI __grid__ for configuring all the BACnet objects of one Edouard@2020: # (generic) BACnet object type (e.g. Binary Value, Analog Input, Multi State Output, ...) Edouard@2020: # This grid is currently displayed within one 'window' controlled by a ObjectEditor Edouard@2020: # object (this organization is not likely to change in the future). Edouard@2020: # Edouard@2020: # The grid uses one line/row per BACnet object, and one column for each property of the BACnet Edouard@2020: # object. The column titles change depending on the specific type of BACnet object being edited Edouard@2020: # (BVObject, BOObject, BIObject, AVObject, AOObject, AIObject, MSVObject, MSOObject, MSIObject). Edouard@2250: # The editor to use for each column is also obtained from that class (e.g. TextEditor, Edouard@2020: # NumberEditor, ...) Edouard@2020: # Edouard@2020: # This class does NOT store the data in the grid. It merely controls its display and editing. Edouard@2020: # The data in the grid is stored within an object of class ObjectTable Edouard@2020: # Edouard@2250: Edouard@2020: def __init__(self, *args, **kwargs): Edouard@2020: CustomGrid.__init__(self, *args, **kwargs) Edouard@2020: Edouard@2020: # Called when a new row is added by clicking Add button Edouard@2250: # call graph: CustomGrid.OnAddButton() --> CustomGrid.AddRow() --> Edouard@2250: # ObjectGrid._AddRow() Edouard@2020: def _AddRow(self, new_row): Edouard@2020: if new_row > 0: Edouard@2020: self.Table.InsertRow(new_row, self.Table.GetRow(new_row - 1).copy()) Edouard@2020: else: Edouard@2020: self.Table.InsertRow(new_row, self.DefaultValue.copy()) Edouard@2250: # start off with invalid object ID Edouard@2250: self.Table.SetValueByName(new_row, "Object Identifier", BACnetObjectID_NUL) Edouard@2020: # Find an apropriate BACnet object ID for the new object. Edouard@2020: # We choose a first attempt (based on object ID of previous line + 1) Edouard@2020: new_object_id = 0 Edouard@2020: if new_row > 0: Edouard@2250: new_object_id = int( Edouard@2250: self.Table.GetValueByName(new_row - 1, "Object Identifier")) Edouard@2020: new_object_id += 1 Edouard@2250: # Check whether the chosen object ID is not already in use. Edouard@2020: # If in use, add 1 to the attempted object ID and recheck... Edouard@2020: while self.Table.FindValueByName("Object Identifier", new_object_id) is not None: Edouard@2020: new_object_id += 1 Edouard@2250: # if reached end of object IDs, cycle back to 0 Edouard@2020: # (remember, we may have started at any inital object ID > 0, so it makes sense to cyclce back to 0) Edouard@2250: # warning: We risk entering an inifinite loop if all object IDs are already used. Edouard@2020: # The likelyhood of this happening is extremely low, (we would need 2^22 elements in the table!) Edouard@2020: # so don't worry about it for now. Edouard@2020: if new_object_id > BACnetObjectID_MAX: Edouard@2020: new_object_id = 0 Edouard@2020: # Set the object ID of the new object to the apropriate value Edouard@2020: # ... and append the ID to the default object name (so the name becomes unique) Edouard@2250: new_object_name = self.DefaultValue.get( Edouard@2250: "Object Name") + " " + str(new_object_id) Edouard@2250: self.Table.SetValueByName( Edouard@2250: new_row, "Object Name", new_object_name) Edouard@2020: self.Table.SetValueByName(new_row, "Object Identifier", new_object_id) Edouard@2020: self.Table.ResetView(self) Edouard@2020: return new_row Edouard@2020: Edouard@2020: # Called when a object ID is changed Edouard@2020: # call graph: ObjectEditor.OnVariablesGridCellChange() --> this method Edouard@2020: # Will check whether there is a duplicate object ID, and highlight it if so. Edouard@2020: def HighlightDuplicateObjectIDs(self): Edouard@2020: if self.Table.GetNumberRows() < 2: Edouard@2250: # Less than 2 rows. No duplicates are possible! Edouard@2020: return Edouard@2020: IDsCount = self.Table.GetObjectIDCount() Edouard@2020: # check ALL object IDs for duplicates... Edouard@2020: for row in range(self.Table.GetNumberRows()): Edouard@2020: obj_id1 = int(self.Table.GetValueByName(row, "Object Identifier")) Edouard@2020: if IDsCount[obj_id1] > 1: Edouard@2250: # More than 1 BACnet object using this ID! Let us Highlight this row with errors... Edouard@2020: # TODO: change the hardcoded column number '0' to a number obtained at runtime Edouard@2020: # that is guaranteed to match the column titled "Object Identifier" Edouard@2020: self.SetCellBackgroundColour(row, 0, ERROR_HIGHLIGHT[0]) Edouard@2250: self.SetCellTextColour(row, 0, ERROR_HIGHLIGHT[1]) Edouard@2250: else: Edouard@2020: self.SetCellBackgroundColour(row, 0, wx.WHITE) Edouard@2250: self.SetCellTextColour(row, 0, wx.BLACK) Edouard@2250: # Refresh the graphical display to take into account any changes we may Edouard@2250: # have made Edouard@2020: self.ForceRefresh() Edouard@2250: return None Edouard@2020: Edouard@2020: # Called when the user changes the name of BACnet object (using the GUI grid) Edouard@2250: # call graph: ObjectEditor.OnVariablesGridCellChange() --> Edouard@2020: # --> BacnetSlaveEditorPlug.HighlightAllDuplicateObjectNames() --> Edouard@2020: # --> ObjectEditor.HighlightDuplicateObjectNames() --> Edouard@2020: # --> (this method) Edouard@2020: # Will check whether there is a duplicate BACnet object name, and highlight it if so. Edouard@2020: # Edouard@2250: # Since the names of BACnet objects must be unique within the whole bacnet server (and Edouard@2020: # not just among the BACnet objects of the same class (e.g. Analog Value, Binary Input, ...) Edouard@2020: # to work properly this method must be passed a list of the names of all BACnet objects Edouard@2020: # currently configured. Edouard@2020: # Edouard@2020: # AllObjectNamesFreq: a dictionary using as key the names of all currently configured BACnet Edouard@2020: # objects, and value the number of objects using this same name. Edouard@2020: def HighlightDuplicateObjectNames(self, AllObjectNamesFreq): Edouard@2020: for row in range(self.Table.GetNumberRows()): Edouard@2020: # TODO: change the hardcoded column number '1' to a number obtained at runtime Edouard@2020: # that is guaranteed to match the column titled "Object Name" Edouard@2020: if AllObjectNamesFreq[self.Table.GetValueByName(row, "Object Name")] > 1: Edouard@2250: # This is an error! Highlight it... Edouard@2020: self.SetCellBackgroundColour(row, 1, ERROR_HIGHLIGHT[0]) Edouard@2250: self.SetCellTextColour(row, 1, ERROR_HIGHLIGHT[1]) Edouard@2250: else: Edouard@2020: self.SetCellBackgroundColour(row, 1, wx.WHITE) Edouard@2250: self.SetCellTextColour(row, 1, wx.BLACK) Edouard@2250: # Refresh the graphical display to take into account any changes we may Edouard@2250: # have made Edouard@2020: self.ForceRefresh() Edouard@2250: return None Edouard@2250: Edouard@2020: Edouard@2020: class ObjectEditor(wx.Panel): Edouard@2020: # This ObjectEditor class: Edouard@2020: # Creates and controls the GUI window for configuring all the BACnet objects of one Edouard@2020: # (generic) BACnet object type (e.g. Binary Value, Analog Input, Multi State Output, ...) Edouard@2020: # This 'window' is currenty displayed within one tab of the bacnet plugin, but this Edouard@2020: # may change in the future! Edouard@2020: # Edouard@2250: # It includes a grid to display all the BACnet objects of its type , as well as the buttons Edouard@2020: # to insert, delete and move (up/down) a BACnet object in the grid. Edouard@2020: # It also includes the sizers and spacers required to lay out the grid and buttons Edouard@2020: # in the wndow. Edouard@2020: # Edouard@2250: Edouard@2020: def __init__(self, parent, window, controller, ObjTable): Edouard@2020: # window: the window in which the editor will open. Edouard@2250: # controller: The ConfigTreeNode object that controlls the data presented by Edouard@2020: # this 'config tree node editor' Edouard@2020: # Edouard@2020: # parent: wx._controls.Notebook Edouard@2020: # window: BacnetSlaveEditorPlug (i.e. beremiz.bacnet.BacnetSlaveEditor.BacnetSlaveEditorPlug) Edouard@2020: # controller: controller will be an object of class Edouard@2020: # FinalCTNClass (i.e. beremiz.ConfigTreeNode.FinalCTNClass ) Edouard@2020: # (FinalCTNClass inherits from: ConfigTreeNode and _BacnetSlavePlug) Edouard@2250: # (For the BACnet plugin, it is easier to think of controller as a _BacnetSlavePlug, Edouard@2020: # as the other classes are generic to all plugins!!) Edouard@2020: # Edouard@2020: # ObjTable: The object of class ObjectTable that stores the data displayed in the grid. Edouard@2020: # This object is instantiated and managed by the _BacnetSlavePlug class. Edouard@2020: # Edouard@2250: self.window = window Edouard@2250: self.controller = controller Edouard@2020: self.ObjTable = ObjTable Edouard@2250: Edouard@2020: wx.Panel.__init__(self, parent) Edouard@2250: Edouard@2020: # The main sizer, 2 rows: top row for buttons, bottom row for 2D grid Edouard@2020: self.MainSizer = wx.FlexGridSizer(cols=1, hgap=10, rows=2, vgap=0) Edouard@2020: self.MainSizer.AddGrowableCol(0) Edouard@2020: self.MainSizer.AddGrowableRow(1) Edouard@2020: Edouard@2250: # sizer placed on top row of main sizer: Edouard@2020: # 1 row; 6 columns: 1 static text, one stretchable spacer, 4 buttons Edouard@2020: controls_sizer = wx.FlexGridSizer(cols=6, hgap=4, rows=1, vgap=5) Edouard@2020: controls_sizer.AddGrowableCol(0) Edouard@2020: controls_sizer.AddGrowableRow(0) Edouard@2250: self.MainSizer.Add(controls_sizer, border=5, flag=wx.GROW | wx.ALL) Edouard@2020: Edouard@2020: # the buttons that populate the controls sizer (itself in top row of the main sizer) Edouard@2020: # NOTE: the _("string") function will translate the "string" to the local language Edouard@2250: controls_sizer.Add( Edouard@2250: wx.StaticText(self, label=_('Object Properties:')), flag=wx.ALIGN_BOTTOM) Edouard@2020: controls_sizer.AddStretchSpacer() Edouard@2020: for name, bitmap, help in [ Edouard@2250: ("AddButton", "add_element", _("Add variable")), Edouard@2250: ("DeleteButton", "remove_element", _("Remove variable")), Edouard@2250: ("UpButton", "up", _("Move variable up")), Edouard@2250: ("DownButton", "down", _("Move variable down"))]: Edouard@2250: button = wx.lib.buttons.GenBitmapButton( Edouard@2250: self, bitmap=GetBitmap(bitmap), Edouard@2250: size=wx.Size(28, 28), Edouard@2250: style=wx.NO_BORDER) Edouard@2020: button.SetToolTipString(help) Edouard@2020: setattr(self, name, button) Edouard@2250: controls_sizer.Add(button) Edouard@2250: Edouard@2250: # the variable grid that will populate the bottom row of the main sizer Edouard@2020: panel = self Edouard@2020: self.VariablesGrid = ObjectGrid(panel, style=wx.VSCROLL) Edouard@2250: # use only to enable drag'n'drop Edouard@2250: # self.VariablesGrid.SetDropTarget(VariableDropTarget(self)) Edouard@2250: self.VariablesGrid.Bind( Edouard@2250: wx.grid.EVT_GRID_CELL_CHANGE, self.OnVariablesGridCellChange) Edouard@2250: # self.VariablesGrid.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnVariablesGridCellLeftClick) Edouard@2250: # self.VariablesGrid.Bind(wx.grid.EVT_GRID_EDITOR_SHOWN, self.OnVariablesGridEditorShown) Edouard@2020: self.MainSizer.Add(self.VariablesGrid, flag=wx.GROW) Edouard@2250: Edouard@2020: # Configure the Variables Grid... Edouard@2250: # do not include a leftmost column containing the 'row label' Edouard@2250: self.VariablesGrid.SetRowLabelSize(0) Edouard@2020: self.VariablesGrid.SetButtons({"Add": self.AddButton, Edouard@2020: "Delete": self.DeleteButton, Edouard@2020: "Up": self.UpButton, Edouard@2020: "Down": self.DownButton}) Edouard@2020: # The custom grid needs to know the default values to use when 'AddButton' creates a new row Edouard@2020: # NOTE: ObjTable.BACnetObjectType will contain the class name of one of the following classes Edouard@2020: # (BVObject, BIObject, BOObject, AVObject, AIObject, AOObject, MSVObject, MSIObject, MSOObject) Edouard@2020: # which inherit from one of (BinaryObject, AnalogObject, MultiSObject) Edouard@2250: self.VariablesGrid.SetDefaultValue( Edouard@2250: self.ObjTable.BACnetObjectType.DefaultValues) Edouard@2020: Edouard@2020: # self.ObjTable: The table that contains the data displayed in the grid Edouard@2250: # This table was instantiated/created in the initializer for class _BacnetSlavePlug Edouard@2020: self.VariablesGrid.SetTable(self.ObjTable) Edouard@2020: self.VariablesGrid.SetEditable(True) Edouard@2020: # set the column attributes (width, alignment) Edouard@2020: # NOTE: ObjTable.BACnetObjectType will contain the class name of one of the following classes Edouard@2020: # (BVObject, BIObject, BOObject, AVObject, AIObject, AOObject, MSVObject, MSIObject, MSOObject) Edouard@2020: # which inherit from one of (BinaryObject, AnalogObject, MultiSObject) Edouard@2020: ColumnAlignments = self.ObjTable.BACnetObjectType.ColumnAlignments Edouard@2250: ColumnSizes = self.ObjTable.BACnetObjectType.ColumnSizes Edouard@2250: for col in range(self.ObjTable.GetNumberCols()): Edouard@2020: attr = wx.grid.GridCellAttr() Edouard@2020: attr.SetAlignment(ColumnAlignments[col], wx.ALIGN_CENTRE) Edouard@2020: self.VariablesGrid.SetColAttr(col, attr) Edouard@2020: self.VariablesGrid.SetColMinimalWidth(col, ColumnSizes[col]) Edouard@2020: self.VariablesGrid.AutoSizeColumn(col, False) Edouard@2250: Edouard@2020: # layout the items in all sizers, and show them too. Edouard@2250: self.SetSizer(self.MainSizer) # Have the wondow 'own' the sizer... Edouard@2250: # self.MainSizer.ShowItems(True) # not needed once the window 'owns' the sizer (SetSizer()) Edouard@2250: # self.MainSizer.Layout() # not needed once the window 'owns' the sizer (SetSizer()) Edouard@2250: Edouard@2020: # Refresh the view of the grid... Edouard@2020: # We ask the table to do that, who in turn will configure the grid for us!! Edouard@2020: # It will configure the CellRenderers and CellEditors taking into account the type of Edouard@2020: # BACnet object being shown in the grid!! Edouard@2020: # Edouard@2020: # Yes, I (Mario de Sousa) know this architecture does not seem to make much sense. Edouard@2020: # It seems to me that the cell renderers etc. should all be configured right here. Edouard@2020: # Unfortunately we inherit from the customTable and customGrid classes in Beremiz Edouard@2020: # (in order to maintain GUI consistency), so we have to adopt their way of doing things. Edouard@2020: # Edouard@2020: # NOTE: ObjectTable.ResetView() (remember, ObjTable is of class ObjectTable) Edouard@2250: # calls ObjectTable._updateColAttrs(), who will do the configuring. Edouard@2020: self.ObjTable.ResetView(self.VariablesGrid) Edouard@2020: Edouard@2020: def RefreshView(self): Edouard@2250: # print "ObjectEditor.RefreshView() called!!!" Edouard@2020: # Check for Duplicate Object IDs is only done within same BACnet object type (ID is unique by type). Edouard@2020: # The VariablesGrid class can handle it by itself. Edouard@2020: self.VariablesGrid.HighlightDuplicateObjectIDs() Edouard@2020: # Check for Duplicate Object Names must be done globally (Object Name is unique within bacnet server) Edouard@2020: # Only the BacnetSlaveEditorPlug can and will handle this. Edouard@2250: # self.window.HighlightAllDuplicateObjectNames() Edouard@2250: Edouard@2250: # Edouard@2020: # Event handlers for the Variables Grid # Edouard@2250: # Edouard@2020: def OnVariablesGridCellChange(self, event): Edouard@2250: col = event.GetCol() Edouard@2250: # print "ObjectEditor.OnVariablesGridCellChange(row=%s, col=%s) Edouard@2250: # called!!!" % (row, col) Edouard@2020: self.ObjTable.ChangesToSave = True Edouard@2020: if self.ObjTable.GetColLabelValue(col) == "Object Identifier": Edouard@2250: # an Object ID was changed => must check duplicate object IDs. Edouard@2020: self.VariablesGrid.HighlightDuplicateObjectIDs() Edouard@2020: if self.ObjTable.GetColLabelValue(col) == "Object Name": Edouard@2250: # an Object Name was changed => must check duplicate object names. Edouard@2020: # Note that this must be done to _all_ BACnet objects, and not just the objects Edouard@2020: # of the same BACnet class (Binary Value, Analog Input, ...) Edouard@2020: # So we have the BacnetSlaveEditorPlug class do it... Edouard@2020: self.window.HighlightAllDuplicateObjectNames() Edouard@2250: # There are changes to save => Edouard@2250: # udate the enabled/disabled state of the 'save' option in the 'file' menu Edouard@2020: self.window.RefreshBeremizWindow() Edouard@2020: event.Skip() Edouard@2020: Edouard@2020: def OnVariablesGridCellLeftClick(self, event): Edouard@2250: pass Edouard@2020: Edouard@2020: def OnVariablesGridEditorShown(self, event): Edouard@2250: pass Edouard@2020: Edouard@2020: def HighlightDuplicateObjectNames(self, AllObjectNamesFreq): Edouard@2020: return self.VariablesGrid.HighlightDuplicateObjectNames(AllObjectNamesFreq) Edouard@2020: Edouard@2020: Edouard@2020: class BacnetSlaveEditorPlug(ConfTreeNodeEditor): Edouard@2020: # inheritance tree Edouard@2020: # wx.SplitterWindow-->EditorPanel-->ConfTreeNodeEditor-->BacnetSlaveEditorPlug Edouard@2020: # Edouard@2020: # self.Controller -> The object that controls the data displayed in this editor Edouard@2020: # In our case, the object of class _BacnetSlavePlug Edouard@2250: Edouard@2020: CONFNODEEDITOR_TABS = [ Edouard@2020: (_("Analog Value Objects"), "_create_AV_ObjectEditor"), Edouard@2020: (_("Analog Output Objects"), "_create_AO_ObjectEditor"), Edouard@2020: (_("Analog Input Objects"), "_create_AI_ObjectEditor"), Edouard@2020: (_("Binary Value Objects"), "_create_BV_ObjectEditor"), Edouard@2020: (_("Binary Output Objects"), "_create_BO_ObjectEditor"), Edouard@2020: (_("Binary Input Objects"), "_create_BI_ObjectEditor"), Edouard@2020: (_("Multi-State Value Objects"), "_create_MSV_ObjectEditor"), Edouard@2020: (_("Multi-State Output Objects"), "_create_MSO_ObjectEditor"), Edouard@2020: (_("Multi-State Input Objects"), "_create_MSI_ObjectEditor")] Edouard@2250: Edouard@2020: def _create_AV_ObjectEditor(self, parent): Edouard@2250: self.AV_ObjectEditor = ObjectEditor( Edouard@2250: parent, self, self.Controler, self.Controler.ObjTables["AV_Obj"]) Edouard@2020: return self.AV_ObjectEditor Edouard@2250: Edouard@2020: def _create_AO_ObjectEditor(self, parent): Edouard@2250: self.AO_ObjectEditor = ObjectEditor( Edouard@2250: parent, self, self.Controler, self.Controler.ObjTables["AO_Obj"]) Edouard@2020: return self.AO_ObjectEditor Edouard@2250: Edouard@2020: def _create_AI_ObjectEditor(self, parent): Edouard@2250: self.AI_ObjectEditor = ObjectEditor( Edouard@2250: parent, self, self.Controler, self.Controler.ObjTables["AI_Obj"]) Edouard@2020: return self.AI_ObjectEditor Edouard@2250: Edouard@2020: def _create_BV_ObjectEditor(self, parent): Edouard@2250: self.BV_ObjectEditor = ObjectEditor( Edouard@2250: parent, self, self.Controler, self.Controler.ObjTables["BV_Obj"]) Edouard@2020: return self.BV_ObjectEditor Edouard@2250: Edouard@2020: def _create_BO_ObjectEditor(self, parent): Edouard@2250: self.BO_ObjectEditor = ObjectEditor( Edouard@2250: parent, self, self.Controler, self.Controler.ObjTables["BO_Obj"]) Edouard@2020: return self.BO_ObjectEditor Edouard@2250: Edouard@2020: def _create_BI_ObjectEditor(self, parent): Edouard@2250: self.BI_ObjectEditor = ObjectEditor( Edouard@2250: parent, self, self.Controler, self.Controler.ObjTables["BI_Obj"]) Edouard@2020: return self.BI_ObjectEditor Edouard@2250: Edouard@2020: def _create_MSV_ObjectEditor(self, parent): Edouard@2250: self.MSV_ObjectEditor = ObjectEditor( Edouard@2250: parent, self, self.Controler, self.Controler.ObjTables["MSV_Obj"]) Edouard@2020: return self.MSV_ObjectEditor Edouard@2250: Edouard@2020: def _create_MSO_ObjectEditor(self, parent): Edouard@2250: self.MSO_ObjectEditor = ObjectEditor( Edouard@2250: parent, self, self.Controler, self.Controler.ObjTables["MSO_Obj"]) Edouard@2020: return self.MSO_ObjectEditor Edouard@2250: Edouard@2020: def _create_MSI_ObjectEditor(self, parent): Edouard@2250: self.MSI_ObjectEditor = ObjectEditor( Edouard@2250: parent, self, self.Controler, self.Controler.ObjTables["MSI_Obj"]) Edouard@2020: return self.MSI_ObjectEditor Edouard@2250: Edouard@2020: def __init__(self, parent, controler, window, editable=True): Edouard@2020: self.Editable = editable Edouard@2020: ConfTreeNodeEditor.__init__(self, parent, controler, window) Edouard@2250: Edouard@2020: def __del__(self): Edouard@2020: self.Controler.OnCloseEditor(self) Edouard@2250: Edouard@2020: def GetConfNodeMenuItems(self): Edouard@2020: return [] Edouard@2250: Edouard@2020: def RefreshConfNodeMenu(self, confnode_menu): Edouard@2020: return Edouard@2020: Edouard@2020: def RefreshView(self): Edouard@2020: self.HighlightAllDuplicateObjectNames() Edouard@2020: ConfTreeNodeEditor.RefreshView(self) Edouard@2020: self. AV_ObjectEditor.RefreshView() Edouard@2020: self. AO_ObjectEditor.RefreshView() Edouard@2020: self. AI_ObjectEditor.RefreshView() Edouard@2020: self. BV_ObjectEditor.RefreshView() Edouard@2020: self. BO_ObjectEditor.RefreshView() Edouard@2020: self. BI_ObjectEditor.RefreshView() Edouard@2020: self.MSV_ObjectEditor.RefreshView() Edouard@2020: self.MSO_ObjectEditor.RefreshView() Edouard@2020: self.MSI_ObjectEditor.RefreshView() Edouard@2020: Edouard@2020: def HighlightAllDuplicateObjectNames(self): Edouard@2020: ObjectNamesCount = self.Controler.GetObjectNamesCount() Edouard@2020: self. AV_ObjectEditor.HighlightDuplicateObjectNames(ObjectNamesCount) Edouard@2020: self. AO_ObjectEditor.HighlightDuplicateObjectNames(ObjectNamesCount) Edouard@2020: self. AI_ObjectEditor.HighlightDuplicateObjectNames(ObjectNamesCount) Edouard@2020: self. BV_ObjectEditor.HighlightDuplicateObjectNames(ObjectNamesCount) Edouard@2020: self. BO_ObjectEditor.HighlightDuplicateObjectNames(ObjectNamesCount) Edouard@2020: self. BI_ObjectEditor.HighlightDuplicateObjectNames(ObjectNamesCount) Edouard@2020: self.MSV_ObjectEditor.HighlightDuplicateObjectNames(ObjectNamesCount) Edouard@2020: self.MSO_ObjectEditor.HighlightDuplicateObjectNames(ObjectNamesCount) Edouard@2020: self.MSI_ObjectEditor.HighlightDuplicateObjectNames(ObjectNamesCount) Edouard@2020: return None Edouard@2250: Edouard@2020: def RefreshBeremizWindow(self): Edouard@2250: # self.ParentWindow is the top level Beremiz class (object) that Edouard@2020: # handles the beremiz window and layout Edouard@2250: Edouard@2250: # Refresh the title of the Beremiz window Edouard@2250: # (it changes depending on whether there are Edouard@2250: # changes to save!! ) Edouard@2250: self.ParentWindow.RefreshTitle() Edouard@2250: Edouard@2250: # Refresh the enabled/disabled state of the Edouard@2250: # entries in the main 'file' menu. Edouard@2250: # ('Save' sub-menu should become enabled Edouard@2250: # if there are changes to save! ) Edouard@2250: self.ParentWindow.RefreshFileMenu() Edouard@2250: Edouard@2250: # self.ParentWindow.RefreshEditMenu() Edouard@2250: # self.ParentWindow.RefreshPageTitles()