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()