From e3ba56f2d2915d6155fbc971d15bb4557a94556a Mon Sep 17 00:00:00 2001 From: Sergio Rubio Manrique Date: Wed, 21 Sep 2016 15:41:14 +0200 Subject: [PATCH 01/54] add clock-events example --- doc/recipes/clock-events.png | Bin 0 -> 26279 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/recipes/clock-events.png diff --git a/doc/recipes/clock-events.png b/doc/recipes/clock-events.png new file mode 100644 index 0000000000000000000000000000000000000000..e9fd96b615c4b14c9587e985b904a4827faa6dd6 GIT binary patch literal 26279 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 Date: Wed, 21 Sep 2016 15:55:25 +0200 Subject: [PATCH 02/54] add clock-events example --- doc/recipes/clock-events-zoom.png | Bin 0 -> 27686 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/recipes/clock-events-zoom.png diff --git a/doc/recipes/clock-events-zoom.png b/doc/recipes/clock-events-zoom.png new file mode 100644 index 0000000000000000000000000000000000000000..d792f5dd00ae806bded1d7be3317d86f2ff79a8f GIT binary patch literal 27686 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 Date: Wed, 21 Sep 2016 16:00:20 +0200 Subject: [PATCH 03/54] Update UseEventsWithTaurus.rst --- doc/recipes/UseEventsWithTaurus.rst | 67 +++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/doc/recipes/UseEventsWithTaurus.rst b/doc/recipes/UseEventsWithTaurus.rst index e69de29b..d506a406 100644 --- a/doc/recipes/UseEventsWithTaurus.rst +++ b/doc/recipes/UseEventsWithTaurus.rst @@ -0,0 +1,67 @@ +PyAlarm Using Events With Taurus +================================ + +Is this approach really Event-Based? +------------------------------------ + +Yes, but not asynchronously. PyAlarm will use Taurus to catch Tango Events and buffer them; but alarms are still triggered by the internal polling thread of PyAlarm. +It means that the PyAlarm.PollingPeriod property effectively filters how often incoming events are processed. + +But, delegating event collection to Taurus allows to not execute read_attribute in the polling thread; allowing to very small PollingPeriod values (10-20 ms) + +As seen in this picture, it allows to have a very fast reaction from the Alarm attributes respect to the trigger: + +.. image:: clock-events-zoom.png + :height: 100px + :width: 200 px + :scale: 50 % + :alt: alternate text + :align: right + +This approach, however, is costly in terms of cpu usage if using polling periods below 100 ms. A pure-asynchronous event implementation of the PyAlarm is still pending. + +Setting up a fast PyAlarm +------------------------- + +We will test events using the CLOCK alarm created in the previous recipe: + + https://github.com/sergirubio/panic/blob/documentation/doc/recipes/CustomAlarms.rst#clock-alarm-triggered-by-time + +Then, we will create the new alarm + +.. code-block:: python + + from panic import AlarmAPI + import fandango as fn + fn.tango.add_new_device('PyAlarm/events','PyAlarm','test/pyalarm/events') + alarms = AlarmAPI() + alarms.add(device='test/pyalarm/events',tag='EVENTS',formula='test/pyalarm/clock/clock') + +Start your device server using Astor, fandango or manually + +.. code-block:: python + + import fandango as fn + fn.Astor('test/pyalarm/events').start_servers(host='your_hostname') + +Then, configure the device properties to read attributes using Taurus and react as fast as possible +Taurus will take care of subscribing to events and update cached values. + +.. code-block:: python + +dtest = alarms.devices['test/pyalarm/events'] + dtest.config['UseTaurus'] = True + dtest.config['AutoReset'] = 0.05 + dtest.config['Enabled'] = 10 + dtest.config['AlarmThreshold'] = 1 + dtest.config['PollingPeriod'] = 0.05 + alarms.put_db_properties(dtest.name,dtest.config) + + This is the result you can expect when showing both alarm attributes (test/pyalarm/clock/clock and test/pyalarm/events/events) in a taurustrend: + + .. image:: clock-events.png + :height: 100px + :width: 200 px + :scale: 50 % + :alt: alternate text + :align: right From 0a0e4b338f67084a7afcae559a28ee39045a8c18 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Wed, 21 Sep 2016 16:01:22 +0200 Subject: [PATCH 04/54] Update UseEventsWithTaurus.rst --- doc/recipes/UseEventsWithTaurus.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/recipes/UseEventsWithTaurus.rst b/doc/recipes/UseEventsWithTaurus.rst index d506a406..1a0d2418 100644 --- a/doc/recipes/UseEventsWithTaurus.rst +++ b/doc/recipes/UseEventsWithTaurus.rst @@ -49,7 +49,7 @@ Taurus will take care of subscribing to events and update cached values. .. code-block:: python -dtest = alarms.devices['test/pyalarm/events'] + dtest = alarms.devices['test/pyalarm/events'] dtest.config['UseTaurus'] = True dtest.config['AutoReset'] = 0.05 dtest.config['Enabled'] = 10 @@ -57,7 +57,7 @@ dtest = alarms.devices['test/pyalarm/events'] dtest.config['PollingPeriod'] = 0.05 alarms.put_db_properties(dtest.name,dtest.config) - This is the result you can expect when showing both alarm attributes (test/pyalarm/clock/clock and test/pyalarm/events/events) in a taurustrend: +This is the result you can expect when showing both alarm attributes (test/pyalarm/clock/clock and test/pyalarm/events/events) in a taurustrend: .. image:: clock-events.png :height: 100px From f48c90fc8ae069e34d42f734ac8c45722c615e1d Mon Sep 17 00:00:00 2001 From: sergirubio Date: Wed, 21 Sep 2016 16:01:53 +0200 Subject: [PATCH 05/54] Update UseEventsWithTaurus.rst --- doc/recipes/UseEventsWithTaurus.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/recipes/UseEventsWithTaurus.rst b/doc/recipes/UseEventsWithTaurus.rst index 1a0d2418..8556dc96 100644 --- a/doc/recipes/UseEventsWithTaurus.rst +++ b/doc/recipes/UseEventsWithTaurus.rst @@ -59,7 +59,7 @@ Taurus will take care of subscribing to events and update cached values. This is the result you can expect when showing both alarm attributes (test/pyalarm/clock/clock and test/pyalarm/events/events) in a taurustrend: - .. image:: clock-events.png +.. image:: clock-events.png :height: 100px :width: 200 px :scale: 50 % From 7b201e09e9826b4959aa4366a8cee368a54a35d8 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Wed, 21 Sep 2016 16:37:19 +0200 Subject: [PATCH 06/54] Update UseEventsWithTaurus.rst --- doc/recipes/UseEventsWithTaurus.rst | 50 +++++++++++++++-------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/doc/recipes/UseEventsWithTaurus.rst b/doc/recipes/UseEventsWithTaurus.rst index 8556dc96..73ac3452 100644 --- a/doc/recipes/UseEventsWithTaurus.rst +++ b/doc/recipes/UseEventsWithTaurus.rst @@ -1,39 +1,21 @@ PyAlarm Using Events With Taurus ================================ -Is this approach really Event-Based? ------------------------------------- - -Yes, but not asynchronously. PyAlarm will use Taurus to catch Tango Events and buffer them; but alarms are still triggered by the internal polling thread of PyAlarm. -It means that the PyAlarm.PollingPeriod property effectively filters how often incoming events are processed. +Setting up a PyAlarm getting Tango events from Taurus +----------------------------------------------------- -But, delegating event collection to Taurus allows to not execute read_attribute in the polling thread; allowing to very small PollingPeriod values (10-20 ms) - -As seen in this picture, it allows to have a very fast reaction from the Alarm attributes respect to the trigger: - -.. image:: clock-events-zoom.png - :height: 100px - :width: 200 px - :scale: 50 % - :alt: alternate text - :align: right - -This approach, however, is costly in terms of cpu usage if using polling periods below 100 ms. A pure-asynchronous event implementation of the PyAlarm is still pending. - -Setting up a fast PyAlarm -------------------------- - -We will test events using the CLOCK alarm created in the previous recipe: +We will test events using the CLOCK alarm created in the previous recipe (polling should be enabled, this example uses polling on CLOCK attribute at 10 ms): https://github.com/sergirubio/panic/blob/documentation/doc/recipes/CustomAlarms.rst#clock-alarm-triggered-by-time -Then, we will create the new alarm +Then, create a new PyAlarm device and the event-based alarm: .. code-block:: python - from panic import AlarmAPI import fandango as fn fn.tango.add_new_device('PyAlarm/events','PyAlarm','test/pyalarm/events') + + from panic import AlarmAPI alarms = AlarmAPI() alarms.add(device='test/pyalarm/events',tag='EVENTS',formula='test/pyalarm/clock/clock') @@ -56,6 +38,7 @@ Taurus will take care of subscribing to events and update cached values. dtest.config['AlarmThreshold'] = 1 dtest.config['PollingPeriod'] = 0.05 alarms.put_db_properties(dtest.name,dtest.config) + dtest.init() This is the result you can expect when showing both alarm attributes (test/pyalarm/clock/clock and test/pyalarm/events/events) in a taurustrend: @@ -65,3 +48,22 @@ This is the result you can expect when showing both alarm attributes (test/pyala :scale: 50 % :alt: alternate text :align: right + +Is this approach really Event-Based? +------------------------------------ + +Yes, but not asynchronously. PyAlarm will use Taurus to catch Tango Events and buffer them; but alarms are still triggered by the internal polling thread of PyAlarm. +It means that the PyAlarm.PollingPeriod property effectively filters how often incoming events are processed. + +But, delegating event collection to Taurus allows to not execute read_attribute in the polling thread; allowing to very small PollingPeriod values (10-20 ms) + +As seen in this picture, it allows to have a very fast reaction from the Alarm attributes respect to the trigger: + +.. image:: clock-events-zoom.png + :height: 100px + :width: 200 px + :scale: 50 % + :alt: alternate text + :align: right + +This approach, however, is costly in terms of cpu usage if using polling periods below 100 ms. A pure-asynchronous event implementation of the PyAlarm is still pending. From f586a60192821c386eb8b2664477641b64548904 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Wed, 21 Sep 2016 16:44:00 +0200 Subject: [PATCH 07/54] Update CustomAlarms.rst --- doc/recipes/CustomAlarms.rst | 52 +++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/doc/recipes/CustomAlarms.rst b/doc/recipes/CustomAlarms.rst index aca4b162..ea5a4c97 100644 --- a/doc/recipes/CustomAlarms.rst +++ b/doc/recipes/CustomAlarms.rst @@ -1,5 +1,8 @@ -Special keys in Alarm formulas ------------------------------- +PANIC Alarm Recipes +=================== + +Special keys used in Alarm formulas +----------------------------------- - DEVICE: PyAlarm device name - DOMAIN,FAMILY,MEMBER: Parts of the device name @@ -23,10 +26,47 @@ Clock: Alarm triggered by time This alarm will be enabled disabled every 5 seconds (if AutoReset=1) -.. code:: +First, create a new PyAlarm device: + +.. code-block:: python + + import fandango as fn + fn.tango.add_new_device('PyAlarm/Clock','PyAlarm','test/pyalarm/clock') + +Add the new alarm (formula will use current time to switch True/False very 5 seconds) + +.. code-block:: python + + from panic import AlarmAPI + alarms = AlarmAPI() + alarms.add(device='test/pyalarm/clock',tag='CLOCK',formula='NOW()%10<5') + +Start your device server using Astor, fandango or manually + +.. code-block:: python + + import fandango as fn + fn.Astor('test/pyalarm/clock').start_servers(host='your_hostname') + +Then, configure the device properties to react every second for both activation and reset: + +.. code-block:: python - from panic import AlarmAPI - panic = AlarmAPI() - panic.add(device='test/pyalarm/clock',tag='CLOCK',formula='t%10<5') + dtest = alarms.devices['test/pyalarm/clock'] + dtest.config['Enabled'] = 1 + dtest.config['AutoReset'] = 1 + dtest.config['AlarmThreshold'] = 1 + dtest.config['PollingPeriod'] = 1 + alarms.put_db_properties(dtest.name,dtest.config) + dtest.init() + +This is the result you can expect when plotting test/pyalarm/clock/CLOCK in a taurustrend: + +.. image:: clock-events.png + :height: 100px + :width: 200 px + :scale: 50 % + :alt: alternate text + :align: right From ba5cb08444f426e00e50d79304b498a93b67c47b Mon Sep 17 00:00:00 2001 From: sergirubio Date: Wed, 21 Sep 2016 16:45:22 +0200 Subject: [PATCH 08/54] Update CustomAlarms.rst --- doc/recipes/CustomAlarms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/recipes/CustomAlarms.rst b/doc/recipes/CustomAlarms.rst index ea5a4c97..d16558d4 100644 --- a/doc/recipes/CustomAlarms.rst +++ b/doc/recipes/CustomAlarms.rst @@ -24,7 +24,7 @@ Special keys used in Alarm formulas Clock: Alarm triggered by time ------------------------------ -This alarm will be enabled disabled every 5 seconds (if AutoReset=1) +This alarm will be enabled/disabled every 5 seconds. First, create a new PyAlarm device: From 092439580007ae128ded8836a252450a82de269c Mon Sep 17 00:00:00 2001 From: sergirubio Date: Wed, 21 Sep 2016 16:45:48 +0200 Subject: [PATCH 09/54] Update CustomAlarms.rst --- doc/recipes/CustomAlarms.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/recipes/CustomAlarms.rst b/doc/recipes/CustomAlarms.rst index d16558d4..b6117e32 100644 --- a/doc/recipes/CustomAlarms.rst +++ b/doc/recipes/CustomAlarms.rst @@ -53,6 +53,7 @@ Then, configure the device properties to react every second for both activation .. code-block:: python dtest = alarms.devices['test/pyalarm/clock'] + dtest.get_config() dtest.config['Enabled'] = 1 dtest.config['AutoReset'] = 1 dtest.config['AlarmThreshold'] = 1 From b0106cfa8fdf0ddcde68e173d7c29ab8dd6bb7f3 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Wed, 21 Sep 2016 16:46:16 +0200 Subject: [PATCH 10/54] Update UseEventsWithTaurus.rst --- doc/recipes/UseEventsWithTaurus.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/recipes/UseEventsWithTaurus.rst b/doc/recipes/UseEventsWithTaurus.rst index 73ac3452..0632adfc 100644 --- a/doc/recipes/UseEventsWithTaurus.rst +++ b/doc/recipes/UseEventsWithTaurus.rst @@ -6,7 +6,7 @@ Setting up a PyAlarm getting Tango events from Taurus We will test events using the CLOCK alarm created in the previous recipe (polling should be enabled, this example uses polling on CLOCK attribute at 10 ms): - https://github.com/sergirubio/panic/blob/documentation/doc/recipes/CustomAlarms.rst#clock-alarm-triggered-by-time + https://github.com/tango-controls/panic/blob/documentation/doc/recipes/CustomAlarms.rst#clock-alarm-triggered-by-time Then, create a new PyAlarm device and the event-based alarm: From 5cef74643dd6930bfef100523a3f5dca1f3cd484 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Wed, 21 Sep 2016 17:21:09 +0200 Subject: [PATCH 11/54] Create HowPyAlarmWorks.rst --- doc/recipes/HowPyAlarmWorks.rst | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 doc/recipes/HowPyAlarmWorks.rst diff --git a/doc/recipes/HowPyAlarmWorks.rst b/doc/recipes/HowPyAlarmWorks.rst new file mode 100644 index 00000000..335b93ae --- /dev/null +++ b/doc/recipes/HowPyAlarmWorks.rst @@ -0,0 +1,39 @@ +How PyAlarm Device Server Works +=============================== + +This document tries to summarize how PyAlarm processes alarms and executes its actions. +A full explanation of alarm syntax and each property is available in the PyAlarm user guide, +but here I provide a summary for convenience. + +The device server behaviour relies on three python objects: AlarmAPI, updateAlarms thread and TangoEval. +Each alarm is independent in terms of formula and receivers; but all alarms within the same PyAlarm device +will share a common evaluation environment determined by PyAlarm properties. + +.. toctree:: + +The AlarmAPI +------------ + +This object encapsulates the access to the alarm configurations database. +Tango Database is used by default, all alarm configurations are stored as device properties +of each declared PyAlarm device (AlarmList, AlarmReceivers, AlarmSeverities). + +The api object allows to load alarms, reconfigure them and transparently move Alarms between PyAlarm devices. + +The updateAlarms thread +----------------------- + +This thread will be executed periodically at a rate specified by the PollingPeriod. +All Enabled alarms will be evaluated at each cycle; and if evaluated to a True value (understood as any value not in (0,"",None,False,[],{})). + +Once an Alarm has been active by a number of cycles equal to the device AlarmThreshold it will become Active. +Then the PyAlarm will process all elements of the AlarmReceivers list. + +The TangoEval engine +-------------------- + +This engine will automatically replace each Tango attribute name in the formula by its value. +It will also provide several methods for searching attribute names in the tango database. + +Amongst other features, all values are kept in a cache with a depth equal to the AlarmThreshold+1. +This cache allows to create alarms using .delta or inspecting the cache for specific behaviors. From 5bfbf9e1bb9da1203a160bb928f4542b6be3727b Mon Sep 17 00:00:00 2001 From: sergirubio Date: Wed, 21 Sep 2016 17:21:55 +0200 Subject: [PATCH 12/54] Update HowPyAlarmWorks.rst --- doc/recipes/HowPyAlarmWorks.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/recipes/HowPyAlarmWorks.rst b/doc/recipes/HowPyAlarmWorks.rst index 335b93ae..532e718f 100644 --- a/doc/recipes/HowPyAlarmWorks.rst +++ b/doc/recipes/HowPyAlarmWorks.rst @@ -6,10 +6,12 @@ A full explanation of alarm syntax and each property is available in the PyAlarm but here I provide a summary for convenience. The device server behaviour relies on three python objects: AlarmAPI, updateAlarms thread and TangoEval. + Each alarm is independent in terms of formula and receivers; but all alarms within the same PyAlarm device will share a common evaluation environment determined by PyAlarm properties. -.. toctree:: +.. contents:: + The AlarmAPI ------------ From b1477bbeacde7896bbc9cbe3d21f560f95fe0e3f Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 22 Sep 2016 12:13:42 +0200 Subject: [PATCH 13/54] Create PyAlarmStartupModes.rst --- doc/recipes/PyAlarmStartupModes.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 doc/recipes/PyAlarmStartupModes.rst diff --git a/doc/recipes/PyAlarmStartupModes.rst b/doc/recipes/PyAlarmStartupModes.rst new file mode 100644 index 00000000..f302c08f --- /dev/null +++ b/doc/recipes/PyAlarmStartupModes.rst @@ -0,0 +1,18 @@ +PyAlarm Startup Modes +===================== + +The PyAlarm Startup is controlled by StartupDelay and Enabled properties. + +StartupDelay will put the PyAlarm in PAUSED state after a restart; +to not start to evaluate formulas immediately but after some seconds, +thus giving time to other devices to start. + +The Enabled property will instead control the notification actions: + +- If False, no notification will be triggered. +- If True, all notifications can be sent once StartupDelay has passed. +- If a Number is given, all notifications triggered between startup and t+Enabled will be ignored. +- Enabled>(AlarmThreshold*PollingPeriod): "Silent restart", activates the Alarms that were presumably +active before a restart; but do not retriggers the notifications. + +Enabled = 120 is the typical case; not triggering notifications until the device has been running for at least 3 minutes. From 05a479bc78c7c3081fea944eb58c79256213cf63 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 22 Sep 2016 12:14:34 +0200 Subject: [PATCH 14/54] Update PyAlarmStartupModes.rst --- doc/recipes/PyAlarmStartupModes.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/recipes/PyAlarmStartupModes.rst b/doc/recipes/PyAlarmStartupModes.rst index f302c08f..418dfba7 100644 --- a/doc/recipes/PyAlarmStartupModes.rst +++ b/doc/recipes/PyAlarmStartupModes.rst @@ -12,7 +12,8 @@ The Enabled property will instead control the notification actions: - If False, no notification will be triggered. - If True, all notifications can be sent once StartupDelay has passed. - If a Number is given, all notifications triggered between startup and t+Enabled will be ignored. -- Enabled>(AlarmThreshold*PollingPeriod): "Silent restart", activates the Alarms that were presumably -active before a restart; but do not retriggers the notifications. +- Enabled>(AlarmThreshold*PollingPeriod): "Silent restart", activates the Alarms that were presumably active before a restart; but do not retriggers the notifications. Enabled = 120 is the typical case; not triggering notifications until the device has been running for at least 3 minutes. + +If Enabled = False or while t < Start+Enabled the PyAlarm State will be DISABLED. From 65e7c6d9d31691387b6196913a041222fda3d73b Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 22 Sep 2016 12:17:57 +0200 Subject: [PATCH 15/54] Update PyAlarmStartupModes.rst --- doc/recipes/PyAlarmStartupModes.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/recipes/PyAlarmStartupModes.rst b/doc/recipes/PyAlarmStartupModes.rst index 418dfba7..edbcaab1 100644 --- a/doc/recipes/PyAlarmStartupModes.rst +++ b/doc/recipes/PyAlarmStartupModes.rst @@ -1,19 +1,19 @@ PyAlarm Startup Modes ===================== -The PyAlarm Startup is controlled by StartupDelay and Enabled properties. +The PyAlarm Startup is controlled by **StartupDelay** and **Enabled** properties. -StartupDelay will put the PyAlarm in PAUSED state after a restart; +**StartupDelay** will put the PyAlarm in *PAUSED* state after a restart; to not start to evaluate formulas immediately but after some seconds, thus giving time to other devices to start. -The Enabled property will instead control the notification actions: +The **Enabled** property will instead control the notification actions: -- If False, no notification will be triggered. -- If True, all notifications can be sent once StartupDelay has passed. -- If a Number is given, all notifications triggered between startup and t+Enabled will be ignored. -- Enabled>(AlarmThreshold*PollingPeriod): "Silent restart", activates the Alarms that were presumably active before a restart; but do not retriggers the notifications. +- If *False*, no notification will be triggered. +- If *True*, all notifications can be sent once **StartupDelay** has passed. +- If a *Number* is given, all notifications triggered between startup and ``t+Enabled`` will be ignored. +- ``Enabled>(AlarmThreshold*PollingPeriod)``: "*Silent restart*", activates the Alarms that were presumably active before a restart; but do not retriggers the notifications. -Enabled = 120 is the typical case; not triggering notifications until the device has been running for at least 3 minutes. +``Enabled = 120`` is the typical case; not triggering notifications until the device has been running for at least 3 minutes. -If Enabled = False or while t < Start+Enabled the PyAlarm State will be DISABLED. +If ``Enabled = False`` or while ``t < Start+Enabled`` the PyAlarm State will be **DISABLED**. From 61cc3d602c6d12d62e4837c9ef2608a123af0e55 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 22 Sep 2016 13:44:47 +0200 Subject: [PATCH 16/54] Create PyAlarm.rst --- doc/PyAlarm.rst | 390 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 doc/PyAlarm.rst diff --git a/doc/PyAlarm.rst b/doc/PyAlarm.rst new file mode 100644 index 00000000..e500c5b6 --- /dev/null +++ b/doc/PyAlarm.rst @@ -0,0 +1,390 @@ +================================ +PyAlarm Device Server User Guide +================================ + +-------------------------------------------------------------------------------------------------------- + +.. contents:: + +Description +=========== + +This device server is used as a alarm logger, it connects to the list of attributes provided and verifies its values. + +Its focused on notifying Alarms by log files, Mail, SMS and (some day in the future) electronic logbook. + +You can acknowledge these alarms by a proper command. + +Alarm Syntax Recipes +==================== + +Alarms are parsed and evaluated using *fandango.TangoEval* class. + +Sending a Test Message at Startup +--------------------------------- + + AlarmList -> DEBUG:True + AlarmDescriptions -> DEBUG:The PyAlarm Device $NAME has been restarted + +Testing a device availability +----------------------------- + +It is done if you put directly the name of the device or its State as a condition by itself. In the second case and alarm will be triggered either if the Pressure is above threshold or the device is not reachable. + +.. code-block:: + + PRESSURE:SR/VC/VGCT/Pressure > 1e-4 + STATE_AND_PRESSURE:?SR/VC/VGCT and SR/VC/VGCT/Pressure > 1e-4 + + +Using Tango state/attribute/value/quality/time/delta/... in formulas +-------------------------------------------------------------------- + +The Alarm syntax allows to add the following clauses to the attribute name (value returned by default): + + some/device/name{/attribute}{.value/all/time/quality/delta/exception} + +*attribute*: if no attribute name is given, then device state is read. + + PLC_Alarm: BL22/CT/EPS-PLC-01 == FAULT + +*value*: default, returns the value of the attribute + + Pressure_Alarm: BL22/CT/EPS-PLC-01/CC1_AF.value > 1e-5 + +*time*: returns the epoch in seconds of the last value read + + Not_Updated: BL22/CT/EPS-PLC-01/CPU_Status.time < (now-60) + +*quality* : returns the tango quality value (ATTR_VALID, ATTR_INVALID, ATTR_WARNING, ATTR_ALARM). + + Temperature_Alarm: BL22/CT/EPS-PLC-01/OP_WBAT_OH01_01_TC11.quality == ATTR_ALARM + +*delta* : returns the variation of the value in the last N=AlarmThreshold reads (stored in TangoEval.cache array of size AlarmThreshold+1) + + Valve_Just_Closed: BL22/CT/EPS-PLC-01/VALVE_11.delta == -1 + +*exception* : True if the attribute is unreadable, False otherwise + + Not_Found: BL22/CT/EPS-PLC-01/I_Dont_Exist.exception + +*all* : returns the raw attribute object as returned by PyTango.DeviceProxy.read_attribute method. + +Creating a periodic self-reset alarm +------------------------------------ + +It's a bit hackish + +.. code-block:: python + + PERIODIC:(FrontEnds/VC/Elotech-01/Temperature and FrontEnds/VC/VGCT-01/P1 and (1920<(now%3600)<3200)) or (ResetAlarm('PERIODIC') and False) + +Enabling search, expression matching and list comprehensions +------------------------------------------------------------ + +Having the syntax ``dom/fam/mem/attr.quality`` whould allow us to call attrs like: + +.. code-block:: python + + any([ATTR_ALARM==s+'.quality' for s in FIND('dom/fam/*/pressure')]) + +One way may be using QUALITY, VALUE, TIME key functions: + +.. code-block:: python + + any([ATTR_ALARM==QUALITY(s) for s in FIND('dom/fam/*/pressure')]) + +The use of FIND allows PyAlarm to prepare a list Taurus models that can be redirected from an
event_received(...)
hook. + +Some list comprehension examples +-------------------------------- + + any([s for s in FIND(SR/ID/SCW01/Cooler*Err*)]) + +equals to + + any(FIND(SR/ID/SCW01/Cooler*Err*)) + +The negate: + + any([s==0 for s in FIND(SR/ID/SCW01/Cooler*Err*)]) + +is equivalent to + +.. code-block:: python + + any(not s for s in FIND(SR/ID/SCW01/Cooler*Err*)]) + +is equivalent to + +.. code-block:: python + + not all(FIND(SR/ID/SCW01/Cooler*Err*)) + +is equivalent to + +.. code-block:: python + + [s for s in FIND(SR/ID/SCW01/Cooler*Err*) if not s] + + +Grouping Alarms in Formulas +--------------------------- + +The proper way is (for readability I use upper case letters for alarms): + +.. code-block:: python + + ALARM_1: just/my/tango/attribute_1 + ALARM_2: just/my/tango/attribute_2 + +then: + +.. code-block:: python + + ALARM_1_OR_2: ALARM_1 or ALARM_2 + +or: + +.. code-block:: python + + ALARM_1_OR_2: any(( ALARM_1 , ALARM_2 )) + +or: + + ALARM_ANY: any( FIND(my/alarm/device/ALARM_*) ) + +Any alarm you declare becomes both a PyAlarm attribute and a variable that you can anywhere (also in other PyAlarm devices). You don't trigger any new read because you just use the result of the formula already evaluated. + +The GROUP is used to tell you that a set of conditions has changed from its previous state. GROUP instead will be triggered not if any is True, but if any of them toggles to True. It forces you to put the whole path to the alarm: + + GROUP(my/alarm/device/ALARM_[12]) + +---- + +PyAlarm Device Properties +========================= + +Distributing Alarms between servers +----------------------------------- + +Alarms can be distributed between PyAlarm servers using the PyAlarm/AlarmsList property. A Panic system works well with 1200+ alarms distributed in 75 devices, with loads between 5 and 70 attrs/device. But instead of thinking in terms of N attrs/pyalarm you must distribute load trying to group all attributes from the same host or subsystem. + +There are two reasons to do that (and also apply to Archiving): + +* When a host is down you'll have a lot of proxy threads in background trying to reconnect to lost devices. If alarms are distributed on rough numbers it becomes a lot of timeouts spreading through the system. When alarms are grouped by host you isolate the problems. + +* Same applies for very event-intensive devices. Devices that generate a lot of information will need lower attrs/pyalarm ratio than devices that do not change so much. + +But, it is a good advice to keep the overall number of alarms in the system below 10K alarms. For manageability of the log system and avoid avalanches of useless information the logical number of alarms should be around or below 1000. + +---- + +Alarm Declaration Properties +---------------------------- + +AlarmList +......... + +Format of alarms will be: + + TAG1:LT/VC/Dev1 + TAG2:LT/VC/Dev1/State + TAG3:LT/VC/Dev1/Pressure > 1e-4 + +NOTE: This property was previously called AlarmsList; it is still loaded if AlarmList is empty for backward compatibility + +AlarmDescriptions +................. + +Description to be included in emails for each alarm. The format is: + + TAG:AlarmDescriptions... + +NOTE: Special Tags like $NAME (for name of PyAlarm device) or $TAG (for name of the Alarm) will be automatically replaced in description. + +AlarmReceivers +.............. + + TAG1:vacuum@accelerator.es,SMS:+34935924381,file:/tmp/err.log + vacuum@accelerator.es:TAG1,TAG2,TAG3 + +Other options are SNAP or ACTION: + + user@cells.es, + SMS:+34666777888, #If SMS sending available + SNAP, #Alarm changes will be recorded in SNAP database. + ACTION(alarm:command,mach/alarm/beep/play_sequence,$DESCRIPTION) + + +Adding ACTION as receiver +......................... + +Executing a command on alarm/disable/reset/acknowledge: + + ACTION(alarm:command,mach/alarm/beep/play_sequence,$DESCRIPTION) + +The syntax allow both attribute/command execution and the usage of multiple typed arguments: + + ACTION(alarm:command,mach/dummy/motor/move,int(1),int(10)) + ACTION(reset:attribute,mach/dummy/motor/position,int(0)) + +Also commands added to the Class property @AllowedCommands@ can be executed: + + ACTION(alarm:system:beep&) + +PhoneBook (not implemented yet) +............................... + +File where alarm receivers aliases are declared; e.g. + + User:user@accelerator.es;SMS:+34666555666 + +Default: `` `$HOME/var/alarm_phone_book.log` `` + +If User and Operator are defined in phonebook, AlarmsReceivers can be: + + TAG2:User,Operator + +---- + +REMINDER / RECOVERED / AUTORESET messages +----------------------------------------- + +Reminder +........ + +If a number of seconds is set, a reminder mail will be sent while the alarm is still active, if 0 no Reminder will be sent. + +AlertOnRecovery +............... + +A message is sent if an alarm is active but the conditions of the attributes return to a safe value. +To enable the message the content of this property must contain 'email', 'sms' or both. If disabled no RECOVERY/AUTO-RESET messages are sent. + +AutoReset +......... + +If a number of seconds is set, the alarm will reset if the conditions are no longer active after the given interval. + +---- + +Snapshot properties +------------------- + +UseSnap +....... + +If false no snapshots will be trigered (unless specifically added to receivers using "SNAP" ), + +CreateNewContexts +................. + +It enables PyAlarm to create new contexts for alarms if no matching context exists in the database. + +---- + +Alarm Configuration Properties +------------------------------ + +(In future releases these properties could be individually configurable for each alarm) + +*Enable* : If False forces the device to Disabled state and avoids messaging. + +*LogFile* : File where alarms are logged Default: `"/tmp/alarm_$NAME.log"` + +*FlagFile* : File where a 1 or 0 value will be written depending if theres active alarms or not.\n
This file can be used by other notification systems. Default: `"/tmp/alarm_ds.nagios"` + +*PollingPeriod* : Periode in seconds. in which all attributes not event-driven will be polled. Default: `60000` + +*MaxAlarmsPerDay* : Max Number of Alarms to be sent each day to the same receiver. Default: `3` + +*AlarmThreshold* : Min number of consecutive Events/Pollings that must trigger an Alarm. Default: `3` + +*FromAddress* : Address that will appear as Sender in mail and SMS Default: `"controls"` + +*SMSConfig* : Arguments for sendSMS command Default: ":" + +*MaxMessagesPerAlarm* : To avoid the previous property to send a lot of messages continuously this property has been added to limit the maximum number of messages to be sent each time that an alarm is enabled/recovered/reset. + +*StartupDelay* : Time that PyAlarm waits before starting the Alarm evaluation threads. + +*EvalTimeout* : Timeout for read_attribute calls, in milliseconds . + +*UseProcess* : To create new OS processes instead of threads. + +---- + +Device Server Example +===================== + +.. code-block:: + + #--------------------------------------------------------- + # SERVER PyAlarm/AssemblyArea, PyAlarm device declaration + #--------------------------------------------------------- + PyAlarm/AssemblyArea/DEVICE/PyAlarm: "LAB/VC/Alarms" + # --- LAB/VC/Alarms properties + LAB/VC/Alarms->AlarmDescriptions: "OVENPRESSURE:The pressure in the Oven exceeds Range",\ + "ADIXENPRESSURE:The pressure in the Roughing Station exceeds Range",\ + "OVENTEMPERATURE:The Temperature of the Oven exceeds Range",\ + "DEBUG:Just for debugging purposes" + LAB/VC/Alarms->AlarmReceivers: OVENPRESSURE:somebody@cells.es,someone_else@cells.es,SMS:+34999666333,\ + ADIXENPRESSURE:somebody@cells.es,someone_else@cells.es,SMS:+34999666333,\ + OVENTEMPERATURE:somebody@cells.es,someone_else@cells.es,SMS:+34999666333,\ + DEBUG:somebody@cells.es + LAB/VC/Alarms->AlarmsList: "OVENPRESSURE:LAB/VC/BestecOven-1/Pressure_mbar > 5e-4",\ + "OVENRUNNING:LAB/VC/BestecOven-1/MaxValue > 70",\ + "ADIXENPRESSURE:LAB/VC/Adixen-01/P1 > 1e-4 and OVENRUNNING",\ + "OVENTEMPERATURE:LAB/VC/BestecOven-1/MaxValue > 220",\ + "DEBUG:OVENRUNNING and not PCISDOWN" + LAB/VC/Alarms->PollingPeriod: 30 + LAB/VC/Alarms->SMSConfig: ... + + +---- + +Mail Messages +============= + + +Format of Alarm message +----------------------- + +.. code-block:: + + Subject: LAB/VC/Alarms: Alarm RECOVERED (OVENTEMPERATURE) + Date: Wed, 12 Nov 2008 11:52:39 +0100 + + TAG: OVENTEMPERATURE + LAB/VC/BestecOven-1/MaxValue > 220 was RECOVERED at Wed Nov 12 11:52:39 2008 + + Alarm receivers are: + somebody@cells.es + someone_else@cells.es + Other Active Alarms are: + DEBUG:Fri Nov 7 18:37:35 2008:OVENRUNNING and not PCISDOWN + OVENRUNNING:Fri Nov 7 18:37:17 2008:LAB/VC/BestecOven-1/MaxValue > 70 + Past Alarms were: + OVENTEMPERATURE:Fri Nov 7 20:49:46 2008 + + +Format of Recovered message +--------------------------- + +.. code-block:: + + Subject: LAB/VC/Alarms: Alarm RECOVERED (OVENTEMPERATURE) + Date: Wed, 12 Nov 2008 11:52:39 +0100 + + TAG: OVENTEMPERATURE + LAB/VC/BestecOven-1/MaxValue > 220 was RECOVERED at Wed Nov 12 11:52:39 2008 + + Alarm receivers are: + somebody@cells.es + someone_else@cells.es + Other Active Alarms are: + DEBUG:Fri Nov 7 18:37:35 2008:OVENRUNNING and not PCISDOWN + OVENRUNNING:Fri Nov 7 18:37:17 2008:LAB/VC/BestecOven-1/MaxValue > 70 + Past Alarms were: + OVENTEMPERATURE:Fri Nov 7 20:49:46 2008 From bcaff17055411700c4d9f64d546405f9b45e8fd4 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 22 Sep 2016 13:46:41 +0200 Subject: [PATCH 17/54] Update PyAlarm.rst --- doc/PyAlarm.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/PyAlarm.rst b/doc/PyAlarm.rst index e500c5b6..122c6449 100644 --- a/doc/PyAlarm.rst +++ b/doc/PyAlarm.rst @@ -77,7 +77,8 @@ It's a bit hackish .. code-block:: python - PERIODIC:(FrontEnds/VC/Elotech-01/Temperature and FrontEnds/VC/VGCT-01/P1 and (1920<(now%3600)<3200)) or (ResetAlarm('PERIODIC') and False) + PERIODIC:(FrontEnds/VC/Elotech-01/Temperature and FrontEnds/VC/VGCT-01/P1 \ + and (1920<(now%3600)<3200)) or (ResetAlarm('PERIODIC') and False) Enabling search, expression matching and list comprehensions ------------------------------------------------------------ From d9f6330cc598abb3704337c0d2f84e0710c1d857 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 22 Sep 2016 13:53:58 +0200 Subject: [PATCH 18/54] Update PyAlarm.rst --- doc/PyAlarm.rst | 117 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 16 deletions(-) diff --git a/doc/PyAlarm.rst b/doc/PyAlarm.rst index 122c6449..098f6114 100644 --- a/doc/PyAlarm.rst +++ b/doc/PyAlarm.rst @@ -15,6 +15,42 @@ Its focused on notifying Alarms by log files, Mail, SMS and (some day in the fut You can acknowledge these alarms by a proper command. +Internal Structure +------------------ + +The device server behaviour relies on three python objects: AlarmAPI, updateAlarms thread and TangoEval. + +Each alarm is independent in terms of formula and receivers; but all alarms within the same PyAlarm device +will share a common evaluation environment determined by PyAlarm properties. + +The AlarmAPI +------------ + +This object encapsulates the access to the alarm configurations database. +Tango Database is used by default, all alarm configurations are stored as device properties +of each declared PyAlarm device (AlarmList, AlarmReceivers, AlarmSeverities). + +The api object allows to load alarms, reconfigure them and transparently move Alarms between PyAlarm devices. + +The updateAlarms thread +----------------------- + +This thread will be executed periodically at a rate specified by the PollingPeriod. +All Enabled alarms will be evaluated at each cycle; and if evaluated to a True value (understood as any value not in (0,"",None,False,[],{})). + +Once an Alarm has been active by a number of cycles equal to the device AlarmThreshold it will become Active. +Then the PyAlarm will process all elements of the AlarmReceivers list. + +The TangoEval engine +-------------------- + +This engine will automatically replace each Tango attribute name in the formula by its value. +It will also provide several methods for searching attribute names in the tango database. + +Amongst other features, all values are kept in a cache with a depth equal to the AlarmThreshold+1. +This cache allows to create alarms using .delta or inspecting the cache for specific behaviors. + + Alarm Syntax Recipes ==================== @@ -23,8 +59,13 @@ Alarms are parsed and evaluated using *fandango.TangoEval* class. Sending a Test Message at Startup --------------------------------- +This alarm formula is just "True" ; therefore will be enabled immediately sendin an email message to test@tester.com + +.. code-block:: + AlarmList -> DEBUG:True AlarmDescriptions -> DEBUG:The PyAlarm Device $NAME has been restarted + AlarmReceivers -> DEBUG: test@tester.com Testing a device availability ----------------------------- @@ -37,35 +78,49 @@ It is done if you put directly the name of the device or its State as a conditio STATE_AND_PRESSURE:?SR/VC/VGCT and SR/VC/VGCT/Pressure > 1e-4 -Using Tango state/attribute/value/quality/time/delta/... in formulas --------------------------------------------------------------------- +Getting Tango state/attribute/value/quality/time/delta in formulas +------------------------------------------------------------------ The Alarm syntax allows to add the following clauses to the attribute name (value returned by default): +.. code-block:: + some/device/name{/attribute}{.value/all/time/quality/delta/exception} *attribute*: if no attribute name is given, then device state is read. +.. code-block:: + PLC_Alarm: BL22/CT/EPS-PLC-01 == FAULT *value*: default, returns the value of the attribute +.. code-block:: + Pressure_Alarm: BL22/CT/EPS-PLC-01/CC1_AF.value > 1e-5 *time*: returns the epoch in seconds of the last value read +.. code-block:: + Not_Updated: BL22/CT/EPS-PLC-01/CPU_Status.time < (now-60) *quality* : returns the tango quality value (ATTR_VALID, ATTR_INVALID, ATTR_WARNING, ATTR_ALARM). +.. code-block:: + Temperature_Alarm: BL22/CT/EPS-PLC-01/OP_WBAT_OH01_01_TC11.quality == ATTR_ALARM *delta* : returns the variation of the value in the last N=AlarmThreshold reads (stored in TangoEval.cache array of size AlarmThreshold+1) +.. code-block:: + Valve_Just_Closed: BL22/CT/EPS-PLC-01/VALVE_11.delta == -1 *exception* : True if the attribute is unreadable, False otherwise +.. code-block:: + Not_Found: BL22/CT/EPS-PLC-01/I_Dont_Exist.exception *all* : returns the raw attribute object as returned by PyTango.DeviceProxy.read_attribute method. @@ -73,7 +128,11 @@ The Alarm syntax allows to add the following clauses to the attribute name (valu Creating a periodic self-reset alarm ------------------------------------ -It's a bit hackish +A simple clock alarm would use the current time and will set AlarmThreshold, PollingPeriod and AutoReset properties. See this example: + + https://github.com/tango-controls/PANIC/blob/documentation/doc/recipes/CustomAlarms.rst#clock-alarm-triggered-by-time + +A single formula clock would be more hackish; this alarm will execute a command on its own formula .. code-block:: python @@ -100,14 +159,20 @@ The use of FIND allows PyAlarm to prepare a list Taurus models that can be redir Some list comprehension examples -------------------------------- +.. code-block:: python + any([s for s in FIND(SR/ID/SCW01/Cooler*Err*)]) equals to +.. code-block:: python + any(FIND(SR/ID/SCW01/Cooler*Err*)) The negate: +.. code-block:: python + any([s==0 for s in FIND(SR/ID/SCW01/Cooler*Err*)]) is equivalent to @@ -153,12 +218,16 @@ or: or: +.. code-block:: python + ALARM_ANY: any( FIND(my/alarm/device/ALARM_*) ) Any alarm you declare becomes both a PyAlarm attribute and a variable that you can anywhere (also in other PyAlarm devices). You don't trigger any new read because you just use the result of the formula already evaluated. The GROUP is used to tell you that a set of conditions has changed from its previous state. GROUP instead will be triggered not if any is True, but if any of them toggles to True. It forces you to put the whole path to the alarm: +.. code-block:: python + GROUP(my/alarm/device/ALARM_[12]) ---- @@ -189,6 +258,8 @@ AlarmList Format of alarms will be: +.. code-block:: + TAG1:LT/VC/Dev1 TAG2:LT/VC/Dev1/State TAG3:LT/VC/Dev1/Pressure > 1e-4 @@ -207,11 +278,15 @@ NOTE: Special Tags like $NAME (for name of PyAlarm device) or $TAG (for name of AlarmReceivers .............. +.. code-block:: + TAG1:vacuum@accelerator.es,SMS:+34935924381,file:/tmp/err.log vacuum@accelerator.es:TAG1,TAG2,TAG3 Other options are SNAP or ACTION: +.. code-block:: + user@cells.es, SMS:+34666777888, #If SMS sending available SNAP, #Alarm changes will be recorded in SNAP database. @@ -223,15 +298,21 @@ Adding ACTION as receiver Executing a command on alarm/disable/reset/acknowledge: +.. code-block:: + ACTION(alarm:command,mach/alarm/beep/play_sequence,$DESCRIPTION) The syntax allow both attribute/command execution and the usage of multiple typed arguments: +.. code-block:: + ACTION(alarm:command,mach/dummy/motor/move,int(1),int(10)) ACTION(reset:attribute,mach/dummy/motor/position,int(0)) Also commands added to the Class property @AllowedCommands@ can be executed: +.. code-block:: + ACTION(alarm:system:beep&) PhoneBook (not implemented yet) @@ -239,12 +320,16 @@ PhoneBook (not implemented yet) File where alarm receivers aliases are declared; e.g. +.. code-block:: + User:user@accelerator.es;SMS:+34666555666 -Default: `` `$HOME/var/alarm_phone_book.log` `` +Default location is: `` `$HOME/var/alarm_phone_book.log` `` If User and Operator are defined in phonebook, AlarmsReceivers can be: +.. code-block:: + TAG2:User,Operator ---- @@ -290,29 +375,29 @@ Alarm Configuration Properties (In future releases these properties could be individually configurable for each alarm) -*Enable* : If False forces the device to Disabled state and avoids messaging. +**Enable** : If False forces the device to Disabled state and avoids messaging. -*LogFile* : File where alarms are logged Default: `"/tmp/alarm_$NAME.log"` +**LogFile** : File where alarms are logged Default: `"/tmp/alarm_$NAME.log"` -*FlagFile* : File where a 1 or 0 value will be written depending if theres active alarms or not.\n
This file can be used by other notification systems. Default: `"/tmp/alarm_ds.nagios"` +**FlagFile** : File where a 1 or 0 value will be written depending if theres active alarms or not.\n
This file can be used by other notification systems. Default: `"/tmp/alarm_ds.nagios"` -*PollingPeriod* : Periode in seconds. in which all attributes not event-driven will be polled. Default: `60000` +**PollingPeriod** : Periode in seconds. in which all attributes not event-driven will be polled. Default: `60000` -*MaxAlarmsPerDay* : Max Number of Alarms to be sent each day to the same receiver. Default: `3` +**MaxAlarmsPerDay** : Max Number of Alarms to be sent each day to the same receiver. Default: `3` -*AlarmThreshold* : Min number of consecutive Events/Pollings that must trigger an Alarm. Default: `3` +**AlarmThreshold** : Min number of consecutive Events/Pollings that must trigger an Alarm. Default: `3` -*FromAddress* : Address that will appear as Sender in mail and SMS Default: `"controls"` +**FromAddress** : Address that will appear as Sender in mail and SMS Default: `"controls"` -*SMSConfig* : Arguments for sendSMS command Default: ":" +**SMSConfig** : Arguments for sendSMS command Default: ":" -*MaxMessagesPerAlarm* : To avoid the previous property to send a lot of messages continuously this property has been added to limit the maximum number of messages to be sent each time that an alarm is enabled/recovered/reset. +**MaxMessagesPerAlarm** : To avoid the previous property to send a lot of messages continuously this property has been added to limit the maximum number of messages to be sent each time that an alarm is enabled/recovered/reset. -*StartupDelay* : Time that PyAlarm waits before starting the Alarm evaluation threads. +**StartupDelay** : Time that PyAlarm waits before starting the Alarm evaluation threads. -*EvalTimeout* : Timeout for read_attribute calls, in milliseconds . +**EvalTimeout** : Timeout for read_attribute calls, in milliseconds . -*UseProcess* : To create new OS processes instead of threads. +**UseProcess** : To create new OS processes instead of threads. ---- From 9cd0b06d3bf71a5914bac84f1f75c70f80cd7272 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 22 Sep 2016 13:54:41 +0200 Subject: [PATCH 19/54] Update PyAlarm.rst --- doc/PyAlarm.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/PyAlarm.rst b/doc/PyAlarm.rst index 098f6114..c02f6b5a 100644 --- a/doc/PyAlarm.rst +++ b/doc/PyAlarm.rst @@ -15,6 +15,8 @@ Its focused on notifying Alarms by log files, Mail, SMS and (some day in the fut You can acknowledge these alarms by a proper command. +.. contents:: + Internal Structure ------------------ From 19739c2cbd76986ea377e10bfab23feb4ca2dda6 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 22 Sep 2016 13:55:39 +0200 Subject: [PATCH 20/54] Update PyAlarm.rst --- doc/PyAlarm.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/PyAlarm.rst b/doc/PyAlarm.rst index c02f6b5a..73471061 100644 --- a/doc/PyAlarm.rst +++ b/doc/PyAlarm.rst @@ -15,10 +15,8 @@ Its focused on notifying Alarms by log files, Mail, SMS and (some day in the fut You can acknowledge these alarms by a proper command. -.. contents:: - Internal Structure ------------------- +================== The device server behaviour relies on three python objects: AlarmAPI, updateAlarms thread and TangoEval. From 34464180a729918436d4f898f02596b09b8e722f Mon Sep 17 00:00:00 2001 From: srubio Date: Fri, 7 Oct 2016 13:06:24 +0200 Subject: [PATCH 21/54] adding summary of requests --- doc/releases/6.0/requests.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 doc/releases/6.0/requests.txt diff --git a/doc/releases/6.0/requests.txt b/doc/releases/6.0/requests.txt new file mode 100644 index 00000000..e8d0c5a2 --- /dev/null +++ b/doc/releases/6.0/requests.txt @@ -0,0 +1,8 @@ +File to summarize requests by different users: + +F.Becheri: + + - Failed alarms should be easy to spot from panic gui + - Users to be alerted if the PyAlarm is dead or idle + + From 36ede5761808115663ca501c419d81f769317453 Mon Sep 17 00:00:00 2001 From: srubio Date: Fri, 7 Oct 2016 18:51:50 +0200 Subject: [PATCH 22/54] Added note on how Taurus subscribes to events --- doc/recipes/tech/HowTaurusPollingWorks.rst | 184 +++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 doc/recipes/tech/HowTaurusPollingWorks.rst diff --git a/doc/recipes/tech/HowTaurusPollingWorks.rst b/doc/recipes/tech/HowTaurusPollingWorks.rst new file mode 100644 index 00000000..e591aeb4 --- /dev/null +++ b/doc/recipes/tech/HowTaurusPollingWorks.rst @@ -0,0 +1,184 @@ + + +Creation of Tango Attribute +=========================== + +self.__subscription_state will keep if the attribute have been subscribed or not + +__subscription_event is a threading.Event + +_events_working is initalized to False + +__chg_evt_id will remember the subscription id +__cfg_evt_id is similar + +TaurusAttribute.__init__ is also called + +cleanUp() +--------- + +will unsubscribeConfEvents and call TaurusAttribute.cleanUp + +write() +------- + +After a write() of a ReadWrite attribute this method is called (value = read_attribute): + + self.poll(single=False, value=result, time=time.time()) + +It is not called if isUsingEvents() returned True + +poll() +------ + +if single: return self.read(cache=False) +else: self.decode(kwargs['value']) + +except: fire Error event +else: fire Periodic event + +subscription_event.set() is called always + +the 'time' argument seems not used at all (taken from attr_value?) + +attr_value returned by decode is a TangoAttrValue + +read() +------ + +if cache = True the cache is checked: + + if delta attr_time < polling_period: + value (or error) is returned + else: + proceeds to next condition + +if cache is False or (not isPollingActive and state in (Pending, Unsubscribed)): + + return read_attribute() + +elif state in (Subscribing,Pending): + event.wait() !?!?! Hungs until subscription finishes? + +last attr_value is returned + + + +ListenerAPI +=========== + +fireRegisterEvent(listener) +--------------------------- + +v = read() +fireEvent(Config/Change,v,listener) + +addListener() +------------------- + +That's the method were subscription is triggered, state +checks are based on initial state, so calls to subscribeEvents +do not affect later checks. + +if first calls TaurusAttribute.addListener(); if fails it returns +It is checked that listeners>=1 + +If it is unsubscribed and it is firstListener ===> subscribeEvents() + +if len(listeners)>1 and (was Subscribed or isPollingActive()): + fireRegisterEvent() + +If Concurrent, event is queued with taurus.Manager.addJob + +return result of TaurusAttribute.addListener + +removeLIstener() +------------------------- + +If it was the last listener it calls unsubscribeEvents() + +returns TaurusAttribute.removeListener() + +isUsingEvents() +---------------------- + +returns state == Subscribed + +subscribeEvents() +-------------------------- + +subscriptionEvent is renewed !?! (previous event is overriden) + +state => Subscribing + +First it tries to subscribe: +chg_evt_id = DeviceProxy.subscribe_event(attr,CHANGE,self,filters=[]) + +If fails then: +state => Pending +activatePolling() +chg_evt_id = DeviceProxy.subscribe_event(attr,CHANGE,self,filters=[],stateless=True) + +stateless=True means that a thread is started to try subscribing every 10 seconds. + +What happens to this thread if device is killed or events disabled!?!? + +Which callback is executed? + +If the attribute is subscribed with NO stateless flag and then the device dies? + +Is the keep alive thread enabled or not? + +unsubscribeEvents() +----------------------------- + +dp.unsubscribe_event() +deactivatePolling() +state => Unsubscribed + +Note, this happens independently of which is the previous state (Subscribed or Pending) +So ... UNSUBSCRIBING ALWAYS DEACTIVATES POLLING!? + +subscribeConfEvents(): +-------------------------------- + +It is very different from subscribing change events. + +The subscription call is always stateless; no state is changed and if it fails (device is dead?) +then a manual call to attribute info is done. + +BUT!, it is using the deviceproxy.attribute_query, that will not work if the device is dead. +A call to the database device should be used instead. + +Then ... what will happend with the configuration event if the attribute does not send that config event? + +? + +pushEvent() +----------------- + +if it is a config event, gets the config and then tries to read the value (.getValueObj(cache=False) + +If it is an attribute event: gets attribute value + state => Subscribed() + deactivatePolling() (it it is not forced) + triggers fireEvent(listeners) (concurrent or not) + +If error and has EVENT_TO_POLLING_EXCEPTIONS + if polling not active: Activate Polling + No event fired for Listeners!! + + elif error: + value = None, get the error + state = Subscribed !? + deactivatePolling() !? + fireEvent(listeners) + + class TangoAttributeEventListener(EventListener) + ------------------------------------------------------------------------ + + A class that stores timestamp for each different value of event received; it may have some application. + + NOTE: This behavior described does not seem to be implemented. + + From e438ba9c0019897617b51f411394bd45c0f207b4 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 16:36:44 +0200 Subject: [PATCH 23/54] Create AlarmDistribution.rst --- doc/releases/6.0/AlarmDistribution.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 doc/releases/6.0/AlarmDistribution.rst diff --git a/doc/releases/6.0/AlarmDistribution.rst b/doc/releases/6.0/AlarmDistribution.rst new file mode 100644 index 00000000..30a10c5a --- /dev/null +++ b/doc/releases/6.0/AlarmDistribution.rst @@ -0,0 +1,21 @@ +Alarms Distribution +=================== + +About distributing load (answer to paul bell, 2014) +--------------------------------------------------- + +We have 1200+ alarms and system works quite well with it. But regarding distribution of PyAlarm devices and servers the rules must be more intelligent. + +Instead of thinking in terms of N attrs/pyalarm you must distribute load trying to group all attributes from the same host or subsystem. + +There are two reasons to do that (and also apply to Archiving): + + - When a host is down you'll have a lot of proxy threads in background trying to reconnect to lost devices. If alarms are distributed on rough numbers it becomes a lot of timeouts spreading through the system. When alarms are grouped by host you isolate the problems. + + - Same applies for very event-intensive devices. Devices that generate a lot of information will need lower attrs/pyalarm ratio than devices that do not change so much. + +Apart of that ... if you have 1000 alarms just for the linac then you may have a wrong specification. I use to say than "all" should be in the order of 10K ; by experience any number about that is too much. If you need more than 10K of a kind what you really need is to add a level of abstraction (do not check all gauges of a vacuum section, just had an attribute where you can read the max value). + +It applies to all Tango systems I've seen (alarms, archiving, save/restore, pool, device tree, ...); if you reach a number above 10K then you must add an abstraction layer. It's not only that you reach a performance limit, also your users will feel too dazed and confused when searching for things. + +e.g. Our accelerator group requested 1200 alarms ... and after some months they asked for a filter to show only the 240 they really care about. From 0b43323f540bc4321d76303dd1c6e3dc762746e0 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 16:37:33 +0200 Subject: [PATCH 24/54] Create AlarmFormulas.rst --- doc/releases/6.0/AlarmFormulas.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 doc/releases/6.0/AlarmFormulas.rst diff --git a/doc/releases/6.0/AlarmFormulas.rst b/doc/releases/6.0/AlarmFormulas.rst new file mode 100644 index 00000000..c8c980af --- /dev/null +++ b/doc/releases/6.0/AlarmFormulas.rst @@ -0,0 +1,25 @@ +Alarm Formulas Examples (Max IV, 2014) +-------------------------------------- + +The proper way is (for readability I use upper case letters for alarms): + + ALARM_1: just/my/tango/attribute_1 + ALARM_2: just/my/tango/attribute_2 + +then: + + ALARM_1_OR_2: ALARM_1 or ALARM_2 + +or: + + ALARM_1_OR_2: any(( ALARM_1 , ALARM_2 )) + +or: + + ALARM_ANY: any( FIND(my/alarm/device/ALARM_*) ) + +Any alarm you declare becomes both a PyAlarm attribute and a variable that you can anywhere (also in other PyAlarm devices). You don't trigger any new read because you just use the result of the formula already evaluated. + +The GROUP is used to tell you that a set of conditions has changed from its previous state. GROUP instead will be triggered not if any is True, but if any of them toggles to True. It forces you to put the whole path to the alarm: + + GROUP(my/alarm/device/ALARM_[12]) From 49c57513baf5dba838603cf891db8efebd858397 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 16:48:11 +0200 Subject: [PATCH 25/54] Update requests.txt --- doc/releases/6.0/requests.txt | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/doc/releases/6.0/requests.txt b/doc/releases/6.0/requests.txt index e8d0c5a2..4ef60bb2 100644 --- a/doc/releases/6.0/requests.txt +++ b/doc/releases/6.0/requests.txt @@ -1,8 +1,31 @@ File to summarize requests by different users: -F.Becheri: +ALBA: - - Failed alarms should be easy to spot from panic gui + - Failed alarms and servers should be easy to spot from panic gui - Users to be alerted if the PyAlarm is dead or idle + - Taurus pop-ups on alarms + +MAXIV: + + - Kibana Integration + - Optimize groups of alarms + - Merge Paul Bell branch: https://sourceforge.net/p/tango-ds/code/HEAD/tree/DeviceClasses/SoftwareSystem/PyAlarm/branches/MAXIV + + SOLEIL: + + - Integration with Vacca + - Integration with the AlarmDB (documents by Katy Saintin). + +Elettra: + + - + +SKA: + + - An statement on PANIC and IEC 62682 compliance + - + + From c88bbc04e65449d98ea72c62965efd6dd0455bde Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 16:56:07 +0200 Subject: [PATCH 26/54] Create Exceptions.rst --- doc/releases/6.0/Exceptions.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 doc/releases/6.0/Exceptions.rst diff --git a/doc/releases/6.0/Exceptions.rst b/doc/releases/6.0/Exceptions.rst new file mode 100644 index 00000000..86d31949 --- /dev/null +++ b/doc/releases/6.0/Exceptions.rst @@ -0,0 +1,16 @@ +Alarm properties that control if exceptions trigger alarms or not ... + + 'RethrowState': + [PyTango.DevBoolean, + "Whether exceptions in State reading will be rethrown.", + [ True ] ],#Overriden by panic.DefaultPyAlarmProperties + + 'RethrowAttribute': + [PyTango.DevBoolean, + "Whether exceptions in Attribute reading will be rethrown.", + [ False ] ],#Overriden by panic.DefaultPyAlarmProperties + + 'IgnoreExceptions': + [PyTango.DevBoolean, + "If True unreadable values will be replaced by None instead of Exception.", + [ True ] ],#Overriden by panic.DefaultPyAlarmProperties From 50bf834a7c9df1fe90428cc348d2ff3b1b65728e Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 17:06:06 +0200 Subject: [PATCH 27/54] Create kibana.rst --- doc/releases/6.0/kibana.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/releases/6.0/kibana.rst diff --git a/doc/releases/6.0/kibana.rst b/doc/releases/6.0/kibana.rst new file mode 100644 index 00000000..81498ada --- /dev/null +++ b/doc/releases/6.0/kibana.rst @@ -0,0 +1,7 @@ +http://www.tango-controls.org/community/forum/post/1123/ + +we've added a tiny feature to PyAlarm which pushes each alarm event as a JSON document to a simple "logger" device (using a command), which in turn stores the event in elasticsearch. The historical data can then be viewed through the kibana web UI, where users can do various filtering and also set up specific views. So far it has been pretty solid, with very low maintenance. + +I'm attaching a kibana screenshot from our controlroom. The UI is a bit strange but powerful once you get used to it. However, the main benefit is that we're not developing it ourselves :) + +Caveat: we're currently using ES 1.X and kibana 3, but the current version of ES is 2.X and kibana 3 is no longer compatible. Kibana 4 is a complete rewrite and works quite differently, with an even more confusing UI. We're not sure whether to migrate or how. From 81e0d777c6abc8ab45184c0a90fc774f7d235da5 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 17:06:40 +0200 Subject: [PATCH 28/54] Create AlarmProperties.rst --- doc/releases/6.0/AlarmProperties.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 doc/releases/6.0/AlarmProperties.rst diff --git a/doc/releases/6.0/AlarmProperties.rst b/doc/releases/6.0/AlarmProperties.rst new file mode 100644 index 00000000..6f418ba9 --- /dev/null +++ b/doc/releases/6.0/AlarmProperties.rst @@ -0,0 +1,15 @@ +* StartupDelay: the device will wait before starting to evaluate the alarms (e.g. giving some time to the system to recover from a powercut). + +* Enabled: if False or 0 the PyAlarm it equals to disabling all alarm actions of the device; if it is True the behavior will be the normal expected; if it has a numeric value (e.g. 120) it means that the device will evaluate the alarms but not execute actions during the first 120 seconds (thus alarms can be activated but no action executed). It is used to prevent a restart of the device to re-execute all alarms that were already active. + +* EvalTimeout: The proxy timeout used when evaluating the attributes (any read attribute slower than timeout will raise exception). + +* AlarmThreshold: number of cycles that an alarm must evaluate to True to be considered active (to avoid alarms on "glitches"). + +* RethrowAttribute/RethrowState: Whether exceptions on reading attributes or states should be rethrown to higher levels, thus causing the alarm to be triggered. By default alarms are enabled if an State attribute is not readable (RethrowState=True), but when a numeric attribute is not readable its value is just replaced by None (RethowAttribute=False) and the formula evaluated normally. + +* Reminder: A new email will be sent every XX seconds if the alarm remains active. When AlertOnRecovery is True an email will be sent also every time when the formula result oscillates from True to False. + +* UseProcess: This is an experimental feature, like UseTaurus and others. In general, I advice you to not modify any parameter that is not detailed in the PyAlarm user guide as you may obtain unexpected results. Some parameters are used to test new features still under development and their behavior may vary between commits. + +Regarding actions on recovery … this option is planned but not yet fully available. Actually just emails are sent when AlertOnRecovery is True. This feature may be implemented in the next 6 months or so but the syntax is still to be decided. From 13372258e38f8abb4fc86b1a52934f7a9dda6a11 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 17:19:22 +0200 Subject: [PATCH 29/54] Update kibana.rst --- doc/releases/6.0/kibana.rst | 121 ++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/doc/releases/6.0/kibana.rst b/doc/releases/6.0/kibana.rst index 81498ada..e41c8b85 100644 --- a/doc/releases/6.0/kibana.rst +++ b/doc/releases/6.0/kibana.rst @@ -5,3 +5,124 @@ we've added a tiny feature to PyAlarm which pushes each alarm event as a JSON do I'm attaching a kibana screenshot from our controlroom. The UI is a bit strange but powerful once you get used to it. However, the main benefit is that we're not developing it ourselves :) Caveat: we're currently using ES 1.X and kibana 3, but the current version of ES is 2.X and kibana 3 is no longer compatible. Kibana 4 is a complete rewrite and works quite differently, with an even more confusing UI. We're not sure whether to migrate or how. + +---- + +PyAlarm Elasticsearch logging +----------------------------- + +The purpose of these changes is to enable PyAlarm to push all alarm events into a database, in this case Elasticsearch but it should be pretty easy to support other databases if desired. The main reason we're going with ES is that it is already established as a platform for storing logs, and has some mature UI tools for this such as Kibana. + +I've tried to compile the changes we've made to PyAlarm to support logging to elasticsearch. The system has been in use for about 1 year now and has worked pretty well. The solution also requires another part, namely the "logger" device. It is a separate device that has an "Alarm" command that takes a JSON string and stores that as a document in elasticsearch. This device is currently undergoing some work, mainly to support newer versions of ES, but we will make it available soon. + +This is not a patch, since I think we've diverged from the main branch of PyAlarm. We tried to make the changes as lcal as we could so it should be easy to just put it in. You can of course refactor this as you like if you think it would fit better in some other way. + +We added an optional string property called "LoggerDevice". It can be configured with the name of a "logger" device. If a logger device is configured, we try to create a proxy to it in "init_device", and if this is successdul, save it in an internal variable called "self._loggerds". If the property is not set, the PyAlarm behavior is not affected. + +Then, there are a few additions to the "send_alarm" method in PyAlarm: +---- +if self._loggerds: + # send alarm data to the logger device + report = self.generate_json(tag_name, message=message, values=values) + self._loggerds.alarm(report) +---- +and "free_alarm" method: +---- +if self._loggerds: + # send alarm data to the logger device + report = self.generate_json(tag_name, message=message, user_comment=comment) + self._loggerds.alarm(report) +---- + +(Note: The communication with the logger device should perhaps be done in an asynchronous way, so that it won't block PyAlarm if the logger device is slow.) + +They call the following method that was added to PyAlarm: + +---- +def generate_json(self, tag_name, message='DETAILS', + values=None, user_comment=None, html=False): + """ + Take an alarm and turn it into a JSON string representation. + The format of this string is dictated by the Logger + device, and follows the shape of the Alarm object. + """ + + # Check alarm + try: + msg = "Generating a json report for alarm {0}" + self.info(msg.format(tag_name)) + alarm = self.Alarms[tag_name] + except KeyError: + return self.warn('Unknown alarm: {0}'.format(tag_name)) + + # Helper function + def cast_dict(dct): + """Convert Boost.Enum objects to strings""" + boost_eval_type = PyTango._PyTango.AttrQuality.__base__ + for key, value in dct.items(): + if isinstance(value, boost_eval_type): + dct[key] = str(value) + + # Build dictionary + try: + self.info("Building dictionary for alarm {0}".format(tag_name)) + _values = values or self.PastValues.get(tag_name) or {} + cast_dict(_values) # Convert Boost.Enum objects to string + report = { + "timestamp": int(time.time() * 1000), + "alarm_tag": tag_name, + "message": message.strip(), + "values": [{"attribute": attr, "value": value} + for attr, value in _values.items()], + "device": self.get_name().strip(), + "description": alarm.parse_description().strip(), + "severity": alarm.parse_severity().strip(), + "instance": alarm.instance, + "formula": alarm.formula.strip() + } + if user_comment: + report["user_comment"] = user_comment + if alarm.recovered: + report["recovered_at"] = int(alarm.recovered * 1000) + if alarm.active: + report["active_since"] = int(alarm.active * 1000) + except Exception as exc: + msg = 'Unexpected exception while building dictionary' + msg = 'for alarm {0}: '.format(tag_name) + return self.warn(msg + repr(exc)) + + # Dump the json string + try: + self.info("Dumping json for alarm {0}".format(tag_name)) + string = json.dumps(report) + except Exception as exc: + msg = 'Unexpected exception while dumping json' + msg = 'for alarm {0}: '.format(tag_name) + return self.warn(msg + repr(exc)) + else: + self.debug(string.replace('%', '%%')) + + # Return json + return string +---- + +Finally, in order for each alarm "event" to be identifiable in the DB, we added an "instance" field to the Alarm object that is a unique ID that ties each activation and deactivation of an alarm together. This is not really necessary for the operation, but it may turn out to be useful in the future. The point is that if you have the activation event of an alarm it makes it easy to find when it was deactivated, and vice versa. + +In Alarm we added the method: +---- + def activate(self): + self.active = time.time() + self.instance = str(uuid4()) # import uuid4 from uuid +---- +And then in PyAlarm replaced the line: +---- + self.Alarms[tag_name].active = time.time() +---- +with +---- + self.Alarms[tag_name].activate() +---- + +That's it! + +/Johan From 11a72612dab97119704ee41307911964168c8014 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 17:42:07 +0200 Subject: [PATCH 30/54] Update requests.txt --- doc/releases/6.0/requests.txt | 190 ++++++++++++++++++++++++++++++++-- 1 file changed, 179 insertions(+), 11 deletions(-) diff --git a/doc/releases/6.0/requests.txt b/doc/releases/6.0/requests.txt index 4ef60bb2..eca9bc25 100644 --- a/doc/releases/6.0/requests.txt +++ b/doc/releases/6.0/requests.txt @@ -1,31 +1,199 @@ File to summarize requests by different users: -ALBA: +ALBA +---- - Failed alarms and servers should be easy to spot from panic gui - Users to be alerted if the PyAlarm is dead or idle - Taurus pop-ups on alarms + - set alarms when the readback is different from the last user input in a given attribute. + - protect againts using "is" in formulas; should be replaced by "==" + - add a command to get last traceback stack in case of Failed alarm. + - persistent disable after restart (via Enabled property!?) + - asynchronous alarm evaluation from Panic GUI preview + - + +ESRF +---- + + - Guide to deploy PANIC/PyAlarm on an empty Debian system -MAXIV: +MAXIV +----- - Kibana Integration - - Optimize groups of alarms - - Merge Paul Bell branch: https://sourceforge.net/p/tango-ds/code/HEAD/tree/DeviceClasses/SoftwareSystem/PyAlarm/branches/MAXIV + - Optimize groups of alarms against host-down exceptions. + - Merge Paul Bell & Johan Forsberg branches: (PRJ/Alerts/branches/maxiv/app-dev-pyalarm.tgz) - SOLEIL: + SOLEIL + ------ - Integration with Vacca - - Integration with the AlarmDB (documents by Katy Saintin). + - Integration with the AlarmDB (see document by Katy Saintin). -Elettra: +TCS +--- + + - Implement multi-tango_host in all queries / formulas. Multiple-host alarm support is one of the important requirement of project. + - Is script execution possible on alarm action ? ACTION(script,path,args) (It should be accompanied of an AllowScripts property and a list of valid script receptors; to prevent a harmful usage of this kind of tool). + - >1500 alarms performance issues (see issues log at the end) + + - Is there any way to write the rule by reading attribute name and device name from custom database ? It means that in defining above rule, I don't have to manually write rule for all parameters. It will read the device and attribute name from database and write the rule accordingly. ===> SEE ALIASES + - ALIASES: The rule must also be configurable, Suppose if Speed1 becomes Speed111, then rule will also be l/m/n/Speed111.quality==ATTR +_ALARM + - set action if alarms recovers in AlarmReceiver. RECOVER(command,...) + - set the action to set the alarm status as "Alarm reset". RECOVER(attribute,a/b/c/alarmstatus,'All Ok') + + - GUI: I have around 150 alarms in my list. When I try to disable single/multiple alarms, its not disabling the selected alarms but when the alarm number is less, then I am able to perform the disable action. Same problem is with acknowledging single/multiple alarms. + + - GUI+CSV: I am trying to import from .csv file. It has around 150 alarms, so the window which opens which says"Choose alarms to import" doesn't have scrollbar attached and I am unable to browse through the lower alarms. Similarly, the problem is with disable and delete button. If you try to delete and disable around 150 alarms, the scrollbar doesnt come. So you couldnt see ok button. I have attached the screenshot of the same problem + + - ACTION(alarm:command, LMC1:10000/lmc/c01/gab/systemCmd,1,2,3). But it is not working. + - command when alarm occurs without providing arguments is not working. + - Multiple-host snap is not working. This is very important requirement for me. But Panic doesnt support getting snaps from multiple-hosts. + - Does Panic support user specific login ? As of now anyone can open the Panic-Gui, but need user login here. Can it be done ? Also have to disable the rule editing/acknowledgement/disable/enable button for some specific user. It means that some user have read only permission and some will have both read-write permission? Can it be done user specific ? + - If Alarm comes, can i snooze it for some time ? + +1.) When alarm comes at CMC, it should archive it in CMC Snap database. + Suppose in one rule around 1350 attributes conditions have been written like this type: + +LMC1:10000/LMC/C01/GAB/Ch01.quality==ATTR_ALARM or LMC2:10000/LMC/C02/GAB/Ch01.quality==ATTR_ALARM or LMC3:10000/LMC/C03/GAB/Ch01.quality==ATTR_ALARM or LMC4:10000/LMC/C04/GAB/Ch01.quality==ATTR_ALARM + +If alarm occurs due to 1 attribute, can that attribute name and value be stored in snap database ? + +2.) When one or two LMC's/ TangoHost are not available, CMC Panic shouldn't slow down. Presently it is happening. + Currently I have defined the condition in CMC for 3 LMC i.e for around 1350 attributes in one rule, it is getting very slow and also gets closed due to segmentation fault. +System shouldn't slow down if any of the device is unreachable. + +3.) Writing condition for 1350 attribute in 1 rules is very tedious, It would be helpful if the grouping works. I will send you the result of the code you sent once we completely migrate to Ubuntu. + +1.) Panic GUI becomes very slow when LMC gets disconnected. There are performance issues. +2.) Is folder-ds released, which would help in logging alarm for multiple hosts as per your below mail +3.) The wild card pattern sometimes doesn't support. I am using it for as many as 1200 attributes. +4.) Is user authentication supported in Panic ? + +Actually i need Panic-GUI login regardless of OS User Login. There will be set of users, for them there will be specific permissions, such as some user would be able to acknowledge the alarm, some won't, some users should be able to edit the rule, some wont have permission to do so. Also I need User log, like, who has acknowledged the alarms, who has added the new alarm, who has edited the rule, i.e. every user activity to be logged. Is this possible ? + - - -SKA: +Elettra +------- - - An statement on PANIC and IEC 62682 compliance - - + - Common API/Specification for Tango properties usage. + - Having the same Alarm in 2 different Pyalarm instances !?!? (G.Scalamera) + - Notify opened clients when there are changes in an alarm configuration. + +SKA/INAF +-------- + + - An statement on PANIC and IEC 62682 compliance (e.g. incorporating PANIC as redundant level) + - Install fandango/PANIC via PIP (Neilen Marais) + - Fast Alarms triggered by single attribute events. +---- + +PERFORMANCE ISSUES +------------------ + +
+I have one Central-Monitoring-Control (cmc:10000) and three lmc's which are LMC1 (LMC1:10000), LMC2(LMC2:10000), LMC3(LMC3:10000).
+
+LMC1 have 5 devices such as: LMC/C01/GAB, LMC/C01/FPS, LMC/C01/FECB, LMC/C01/SERVO, LMC/C01/OFCSNT)
+LMC2 have 5 devices such as: LMC/C02/GAB, LMC/C02/FPS, LMC/C02/FECB, LMC/C02/SERVO, LMC/C02/OFCSNT)
+LMC3 have 5 devices such as: LMC/C03/GAB, LMC/C03/FPS, LMC/C03/FECB, LMC/C03/SERVO, LMC/C03/OFCSNT)
+Each device have around 70 attributes.
+So one LMC have around 70*5=350 attributes.
+
+Now I have to define a rule for all the attributes of LMCs in CMC. I tried the easier way by grouping.
+
+I tried to define rule as:
+
+any([t==ATTR_ALARM for t in FIND(LMC1:10000/LMC/C01/*/*.quality)]) or any([t==ATTR_ALARM for t in FIND(LMC2:10000/LMC/C02/*/*.quality)]) or any([t==ATTR_ALARM for t in FIND(LMC3:10000/LMC/C03/*/*.quality)])
+
+But the result is:
+
+test/alarms/1: DevFailed[
+
+DevError[
+
+desc = TRANSIENT CORBA system exception: TRANSIENT_CallTimedout
+
+origin = Connection::command_inout()
+
+reason = API_CorbaException
+
+severity = ERR]
+
+
+DevError[
+
+desc = Timeout (500 mS) exceeded on device test/alarms/1, command evaluateFormula
+
+origin = Connection::command_inout()
+
+reason = API_DeviceTimedOut
+
+severity = ERR]
+
+]
+
+Then I tried the long way which is individually defining the rule for all the attributes of all LMCs.
+
+For example, below is the rule:
+
+LMC1:10000/LMC/C01/GAB/ch1.quality==ATTR_ALARM or
+LMC2:10000/LMC/C02/GAB/ch1.quality==ATTR_ALARM or
+LMC3:10000/LMC/C03/GAB/ch1.quality==ATTR_ALARM or
+LMC1:10000/LMC/C01/GAB/ch2.quality==ATTR_ALARM or
+LMC2:10000/LMC/C02/GAB/ch2.quality==ATTR_ALARM or
+LMC3:10000/LMC/C03/GAB/ch2.quality==ATTR_ALARM or
+LMC1:10000/LMC/C01/GAB/ch3.quality==ATTR_ALARM or
+LMC2:10000/LMC/C02/GAB/ch3.quality==ATTR_ALARM or
+LMC3:10000/LMC/C03/GAB/ch3.quality==ATTR_ALARM
+
+Like the above I typed for around all LMCs attributes 750 times which was very tedious.
+
+and the result is same as above which is:
+
+test/alarms/1: DevFailed[
+
+DevError[
+
+desc = TRANSIENT CORBA system exception: TRANSIENT_CallTimedout
+
+origin = Connection::command_inout()
+
+reason = API_CorbaException
+
+severity = ERR]
+
+
+DevError[
+
+desc = Timeout (500 mS) exceeded on device test/alarms/1, command evaluateFormula
+
+origin = Connection::command_inout()
+
+reason = API_DeviceTimedOut
+
+severity = ERR]
+
+]
+
+Because of the above error, rule becomes true and is in alarm condition even if it is not.
+
+Ideally it shouldn't happen, because my future requirement is like , I
+would be having 30 LMCs, so it would be very tedious job to configure the
+rules for all the lmcs unless grouping works.
+
+Secondly, the GUI-tool has become very slow and hangs and then gets closed and provides segmentation fault. Also the alarm device server is using too much memory, which slows down the system.
+
+Thirdly, Can i Take action from CMC to any LMC command or LMC attribute ? Below is the syntax i tried:
+
+ACTION(alarm:command, LMC1:10000/lmc/c01/gab/systemCmd,1,2,3). But it is not working.
 
+Fourth, Executing command when alarm occurs without providing arguments is not working.
 
+Fifth, Multiple-host snap  is not working. This is very important requirement for me. But Panic doesnt support getting snaps from multiple-hosts.
+
From 71dd536b698b4ffa4b6a8bceffc30345cd407237 Mon Sep 17 00:00:00 2001 From: srubio Date: Fri, 14 Oct 2016 11:06:55 +0200 Subject: [PATCH 31/54] Describe Taurus Polling Switching --- doc/recipes/tech/HowTaurusPollingWorks.rst | 46 +++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/doc/recipes/tech/HowTaurusPollingWorks.rst b/doc/recipes/tech/HowTaurusPollingWorks.rst index e591aeb4..6bc8f7a8 100644 --- a/doc/recipes/tech/HowTaurusPollingWorks.rst +++ b/doc/recipes/tech/HowTaurusPollingWorks.rst @@ -1,7 +1,25 @@ -Creation of Tango Attribute -=========================== +Creation of Tango Attribute (Taurus 4.0) +================================= + +What's the difference between enable polling, activate polling and force polling? + +By default, polling is enabled and not active. If by any reason it is disabled; calls to activatePolling() will do nothing. But, an enablePolling(force=True) will also activate polling). + +Activating polling means that this method is called: +self.factory().addAttributeToPolling(self, self.getPollingPeriod()) + +But this is a protected member that is not directly called by activatePolling(); it will call changePollingPeriod() instead that will not trigger the polling if it wasn't already active. + +!?!?: Then, a call to activatePolling(period) will first activate (changing the period), then enable it. BUT!, polling will be activated only if it was already active!!. If activatePolling() is called without force=True then it in fact does nothing!?!? It activates polling only if was already active; and then enable it but not start it !?!? + +For me, it seems a Bug or a dangerous uncoherency: + +self._activatePolling() => activates polling if it was enabled +self.activatePolling() => enables polling but does not activate it unless you add force=True argument (which is not documented in method description). In fact, it also has an unsubscribe_evts argument which is never used !? + +... self.__subscription_state will keep if the attribute have been subscribed or not @@ -14,6 +32,10 @@ __cfg_evt_id is similar TaurusAttribute.__init__ is also called +In Taurus, the parent is the Taurus Device +the DevHWObj is the Device Proxy +The ValueObj is the attr_value returned by read_attribute + cleanUp() --------- @@ -31,13 +53,15 @@ It is not called if isUsingEvents() returned True poll() ------ +It is the method call by polling threads; it is a read(cache=False)+fireEvent() + if single: return self.read(cache=False) -else: self.decode(kwargs['value']) +else: self.decode(kwargs['value']) #Value can be forced from an external source except: fire Error event else: fire Periodic event -subscription_event.set() is called always +subscription_event.set() is called always #read() calls are blocked by this event if attributes were in Subscribing or Pending state; IS IT A BUG? the 'time' argument seems not used at all (taken from attr_value?) @@ -57,12 +81,24 @@ if cache is False or (not isPollingActive and state in (Pending, Unsubscribed)): return read_attribute() -elif state in (Subscribing,Pending): +elif SubscriptionState in (Subscribing,Pending): event.wait() !?!?! Hungs until subscription finishes? last attr_value is returned + THEN: + A read() will first check the cache, if the value is not older than polling period, it is returned. + If it was received by an event, it will be returned if the subscription state is not pending, unsubscribed or subscribing. If it is, a HW read or a .wait() may be called until an event is processed. + + BUT: a Pending state just means that a subscribe was tried on a device that has no events; so most devices will have a Pending state. It means that a read(cache=True) that gets an attr_value not updated will hung in a .wait() until the next polling is executed. If the polling thread is dead, it may be forever. + Note, that all attributes in polling will be always in PendingSubscribe state; they will switch to Subscribed once the first event arrives; at this point the polling will be deactivated. + + A read() will never activatePolling(); it can be done only by a push_event() receiving an error event listed in the EVENT_TO_POLLING_EXCEPTIONS; or if the first subscribe_event call fails. + + Other confusing thing is that subscribeEvents does not check if the attribute was already subscribed. So it can override the existing ID! + +Also, unsubscribing events deactivates polling. It should happen only at cleanUp() or when removing the last listener. But if the state was pending it would disable completely the update of attributes; without checking if it was the last listener.. When adding the first listener it will subscribe to events again (and/or activatePolling). ListenerAPI =========== From 5b3ff9727228e664975118a8eee121f125afcafb Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 22 Sep 2016 16:07:52 +0200 Subject: [PATCH 32/54] Update README --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index c7ebf798..3170e0f2 100644 --- a/README +++ b/README @@ -2,7 +2,7 @@ # http://www.tango-controls.org/community/projects/panic # https://svn.code.sf.net/p/tango-ds/code/DeviceClasses/SoftwareSystem/PyAlarm/tags/latest - +PANIC IS SUPPORTED ON LINUX ONLY, WINDOWS IS NO LONGER SUPPORTED (At least for the current and next release) Panic package contains the python AlarmAPI for managing the PyAlarm device servers from a client application or a python shell. The panic module is used by PyAlarm, Panic Toolbar and Panic GUI. From 7677ce799ced786dd3a9a859c2cd88d74314bc09 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 22 Sep 2016 16:08:27 +0200 Subject: [PATCH 33/54] Update README --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 3170e0f2..f186ecf1 100644 --- a/README +++ b/README @@ -2,7 +2,7 @@ # http://www.tango-controls.org/community/projects/panic # https://svn.code.sf.net/p/tango-ds/code/DeviceClasses/SoftwareSystem/PyAlarm/tags/latest -PANIC IS SUPPORTED ON LINUX ONLY, WINDOWS IS NO LONGER SUPPORTED (At least for the current and next release) +PANIC IS SUPPORTED ON LINUX ONLY, WINDOWS IS NOT SUPPORTED (At least for the current and next release) Panic package contains the python AlarmAPI for managing the PyAlarm device servers from a client application or a python shell. The panic module is used by PyAlarm, Panic Toolbar and Panic GUI. From 3a1afb1016c32c6d98f8ff31c6f5645ff6c47307 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Fri, 23 Sep 2016 09:07:02 +0200 Subject: [PATCH 34/54] Update README --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index f186ecf1..881b5e5c 100644 --- a/README +++ b/README @@ -2,7 +2,7 @@ # http://www.tango-controls.org/community/projects/panic # https://svn.code.sf.net/p/tango-ds/code/DeviceClasses/SoftwareSystem/PyAlarm/tags/latest -PANIC IS SUPPORTED ON LINUX ONLY, WINDOWS IS NOT SUPPORTED (At least for the current and next release) +PANIC IS TESTED ON LINUX ONLY, WINDOWS/MAC MAY NOT BE FULLY SUPPORTED IN MASTER BRANCH Panic package contains the python AlarmAPI for managing the PyAlarm device servers from a client application or a python shell. The panic module is used by PyAlarm, Panic Toolbar and Panic GUI. From 50385934d5361ebf37ce55a19dd5d7a91080472e Mon Sep 17 00:00:00 2001 From: srubio Date: Thu, 29 Sep 2016 19:00:50 +0200 Subject: [PATCH 35/54] launcher scripts reorganized --- {panic/ds => bin}/PyAlarm | 0 bin/panic | 6 ++++++ setup.py | 5 ++++- 3 files changed, 10 insertions(+), 1 deletion(-) rename {panic/ds => bin}/PyAlarm (100%) create mode 100755 bin/panic diff --git a/panic/ds/PyAlarm b/bin/PyAlarm similarity index 100% rename from panic/ds/PyAlarm rename to bin/PyAlarm diff --git a/bin/panic b/bin/panic new file mode 100755 index 00000000..4dabdb2c --- /dev/null +++ b/bin/panic @@ -0,0 +1,6 @@ +#!/bin/sh + +PANIC=$(python -c "import imp;print(imp.find_module('panic')[1])") +CMD=$PANIC/gui/gui.py +echo $CMD $* +python $CMD $* diff --git a/setup.py b/setup.py index 04505565..ffd9cc35 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,10 @@ package_data = {'': ['VERSION']} -scripts = ['./panic/ds/PyAlarm',] +scripts = [ + './bin/PyAlarm', + './bin/panic', + ] entry_points = { 'console_scripts': [ From c4d91e7239b082250088b3929289d41de925b400 Mon Sep 17 00:00:00 2001 From: srubio Date: Thu, 29 Sep 2016 19:02:51 +0200 Subject: [PATCH 36/54] launcher scripts reorganized --- bin/{panic => panic-gui} | 0 setup.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename bin/{panic => panic-gui} (100%) diff --git a/bin/panic b/bin/panic-gui similarity index 100% rename from bin/panic rename to bin/panic-gui diff --git a/setup.py b/setup.py index ffd9cc35..613a7037 100644 --- a/setup.py +++ b/setup.py @@ -19,12 +19,12 @@ scripts = [ './bin/PyAlarm', - './bin/panic', + './bin/panic-gui', ] entry_points = { 'console_scripts': [ - 'panic-gui=panic.gui.gui:main_gui', + #'panic-gui=panic.gui.gui:main_gui', ], } From 49f9bd20ce985615134e8b2d22a846acbd1abb0b Mon Sep 17 00:00:00 2001 From: srubio Date: Fri, 30 Sep 2016 07:51:37 +0200 Subject: [PATCH 37/54] solved bug on New button and empty database --- panic/gui/gui.py | 91 +++++++++++++++++++++++++++----------------- panic/gui/widgets.py | 20 ++++++++-- 2 files changed, 73 insertions(+), 38 deletions(-) diff --git a/panic/gui/gui.py b/panic/gui/gui.py index 052945ae..abaaddca 100644 --- a/panic/gui/gui.py +++ b/panic/gui/gui.py @@ -119,6 +119,7 @@ def __init__(self, parent=None, filters='*', options=None, mainwindow=None): self.setSecondCombo() self._ui.infoLabel0_1.setText(self._ui.contextComboBox.currentText()) self.updateStatusLabel() + print('__init__ done') #if not SNAP_ALLOWED: #Qt.QMessageBox.critical(self,"Unable to load SNAP",'History Viewer Disabled!', QtGui.QMessageBox.AcceptRole, QtGui.QMessageBox.AcceptRole) @@ -233,6 +234,7 @@ def setAlarmRowModel(self,nr,obj,alarm,use_list): self.updateStatusLabel() def connectAll(self): + trace('connecting') #QtCore.QObject.connect(self.refreshTimer, QtCore.SIGNAL("timeout()"), self.onRefresh) if self.USE_EVENT_REFRESH: QtCore.QObject.connect(self,QtCore.SIGNAL("valueChanged"),self.hurry) #Qt.QObject.connect(self._ui.actionExpert,Qt.SIGNAL("changed()"),self.setExpertView) @@ -261,6 +263,7 @@ def connectAll(self): #QtCore.QObject.connect(self._ui.listWidget, QtCore.SIGNAL("currentRowChanged(int)"), self.setAlarmData) QtCore.QObject.connect(self._ui.buttonClose,QtCore.SIGNAL("clicked()"), self.close) + trace('all connected') def printRows(self): for row in self._ui.listWidget.selectedItems(): @@ -304,26 +307,35 @@ def hurry(self): @Catched def onReload(self): # THIS METHOD WILL NOT MODIFY THE LIST IF JUST FILTERS HAS CHANGED; TO UPDATE FILTERS USE onRefresh INSTEAD - trace('onReload(%s)'%self.RELOAD_TIME) - print '+'*80 - now = time.time() - trace('%s -> AlarmGUI.onReload() after %f seconds'%(now,now-self.last_reload)) - self.last_reload=now - self.api.load() - AlarmRow.TAG_SIZE = 1+max(len(k) for k in self.api.keys()) - #Removing deleted/renamed alarms - for tag in self.AlarmRows.keys(): - if tag not in self.api: - self.removeAlarmRow(tag) - #Updating the alarm list - self.buildList(changed=False) - if self.changed: self.showList() - #Triggering refresh timers - self.reloadTimer.setInterval(self.RELOAD_TIME) - self.refreshTimer.setInterval(self.REFRESH_TIME) - if not self._connected: - self._connected = True - self.connectAll() + try: + trace('onReload(%s)'%self.RELOAD_TIME) + print '+'*80 + now = time.time() + trace('%s -> AlarmGUI.onReload() after %f seconds'%(now,now-self.last_reload)) + self.last_reload=now + self.api.load() + + if self.api.keys(): + AlarmRow.TAG_SIZE = 1+max(len(k) for k in self.api.keys()) + + #Removing deleted/renamed alarms + for tag in self.AlarmRows.keys(): + if tag not in self.api: + self.removeAlarmRow(tag) + + #Updating the alarm list + self.buildList(changed=False) + if self.changed: self.showList() + + #Triggering refresh timers + self.reloadTimer.setInterval(self.RELOAD_TIME) + self.refreshTimer.setInterval(self.REFRESH_TIME) + + if not self._connected: + self._connected = True + self.connectAll() + except: + trace(traceback.format_exc()) @Catched def onRefresh(self): @@ -701,14 +713,21 @@ def onView(self): return self.onEdit(edit=False) def onNew(self): - trace('onNew()') - if self._ui.listWidget.currentItem(): - self._ui.listWidget.currentItem().setSelected(False) - form = AlarmForm(self.parent()) - form.connect(form,Qt.SIGNAL('valueChanged'),self.hurry) - form.onNew() - form.show() - return form + try: + trace('onNew()') + if not self.api.devices: + v = Qt.QMessageBox.warning(self,'Warning','You should create a PyAlarm device first (using jive or config panel)!',Qt.QMessageBox.Ok) + return + if self._ui.listWidget.currentItem(): + self._ui.listWidget.currentItem().setSelected(False) + form = AlarmForm(self.parent()) + trace('form') + form.connect(form,Qt.SIGNAL('valueChanged'),self.hurry) + form.onNew() + form.show() + return form + except: + traceback.print_exc() def onConfig(self): self.dac = dacWidget(device=self.getCurrentAlarm().device) @@ -915,15 +934,19 @@ def onDisStateChanged(self,checked=False): def main(args=[]): import widgets from taurus.qt.qtgui import resource -# print os.getenv('TANGO_HOST') - print '='*80 - trace(' Launching Panic ...') - print '='*80 + from taurus.qt.qtgui.application import TaurusApplication + opts = [a for a in args if a.startswith('--')] args = [a for a in args if not a.startswith('--')] URL = 'http://www.cells.es/Intranet/Divisions/Computing/Controls/Help/Alarms/panic' - uniqueapp = Qt.QApplication([]) + #uniqueapp = Qt.QApplication([]) + uniqueapp = TaurusApplication(opts) + + print '='*80 + trace(' Launching Panic ...') + print '='*80 + if '--calc' in opts: args = args or [''] form = AlarmPreview(*args) @@ -997,4 +1020,4 @@ def main_gui(): sys.exit(n) if __name__ == "__main__": - main_gui() \ No newline at end of file + main_gui() diff --git a/panic/gui/widgets.py b/panic/gui/widgets.py index 7b1dd16f..a1aa9c79 100644 --- a/panic/gui/widgets.py +++ b/panic/gui/widgets.py @@ -3,12 +3,16 @@ from PyQt4 import Qt, QtCore, QtGui import taurus,fandango,fandango.qt from taurus.qt.qtgui.base import TaurusBaseWidget +from taurus.qt.qtgui.container import TaurusWidget from taurus.qt.qtgui import resource +from taurus.core.util import Logger import panic from panic.widgets import AlarmValueLabel import getpass +dummies = [] + def get_user(): try: return getpass.getuser() @@ -28,11 +32,19 @@ def print_clean(s): print(clean_str(s)) TRACE_LEVEL = -1 -def trace(msg,head='',level=0,clean=False): +def trace(msg,head='',level=0,clean=False,use_taurus=False): if level > TRACE_LEVEL: return if type(head)==int: head,level = '',head - (print_clean if clean else fandango.printf)( - fandango.time2str()+':'+str(head)+('\t'*level or ' ')+str(msg)) + msg = fandango.time2str()+':'+str(head)+('\t'*level or ' ')+str(msg) + if use_taurus: + if not dummies: + dummies.append(Logger()) + dummies[0].setLogLevel('INFO') + print dummies[0] + dummies[0].info(msg) + dummies[0].error(msg) + else: + (print_clean if clean else fandango.printf)(msg) return def get_bold_font(points=8): @@ -637,4 +649,4 @@ def mouseDoubleClickEvent(self,event): qapp = Qt.QApplication(sys.argv) form = AlarmPreview(*sys.argv[1:]) form.show() - qapp.exec_() \ No newline at end of file + qapp.exec_() From 8d054b5b2beceab5bef7abb1719cbbd9f60b6bb8 Mon Sep 17 00:00:00 2001 From: srubio Date: Fri, 30 Sep 2016 08:29:59 +0200 Subject: [PATCH 38/54] Add button to create new devices --- panic/gui/devattrchange.py | 43 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/panic/gui/devattrchange.py b/panic/gui/devattrchange.py index ccda3d89..52eb85a8 100644 --- a/panic/gui/devattrchange.py +++ b/panic/gui/devattrchange.py @@ -40,6 +40,9 @@ def devattrchangeSetupUi(self, Form): self.refreshButton = QtGui.QPushButton(Form) self.refreshButton.setObjectName("refreshButton") self.GridLayout.addWidget(self.refreshButton, 2, 0, 1, 1) + self.newDevice = QtGui.QPushButton(Form) + self.newDevice.setObjectName("newDevice") + self.GridLayout.addWidget(self.newDevice, 3, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) @@ -48,10 +51,15 @@ def retranslateUi(self, Form): self.refreshButton.setText(QtGui.QApplication.translate("Form", "Refresh", None, QtGui.QApplication.UnicodeUTF8)) self.refreshButton.setIcon(getThemeIcon("view-refresh")) self.refreshButton.setToolTip("Refresh list") + self.newDevice.setText(QtGui.QApplication.translate("Form", "Create New", None, QtGui.QApplication.UnicodeUTF8)) + self.newDevice.setIcon(getThemeIcon("new")) + self.newDevice.setToolTip("Add a new PyAlarm device") + QtCore.QObject.connect(self.tableWidget, QtCore.SIGNAL("itemChanged(QTableWidgetItem *)"), self.onEdit) QtCore.QObject.connect(self.deviceCombo, QtCore.SIGNAL("currentIndexChanged(QString)"), self.buildList) QtCore.QObject.connect(self.refreshButton, QtCore.SIGNAL("clicked()"), self.buildList) + QtCore.QObject.connect(self.newDevice, QtCore.SIGNAL("clicked()"), self.onNew) Form.resize(430, 600) def setDevCombo(self,device=None): @@ -75,7 +83,10 @@ def buildList(self,device=None): else: self.deviceCombo.setCurrentIndex(index) device = str(device) - data=self.api.devices[device].get_config(True) #get_config() already manages extraction and default values replacement + if self.api.devices: + data=self.api.devices[device].get_config(True) #get_config() already manages extraction and default values replacement + else: + data = {} print '%s properties: %s' % (device,data) rows=len(data) self.tableWidget.setColumnCount(2) @@ -93,6 +104,34 @@ def buildList(self,device=None): self.tableWidget.setItem(row, col, item) self.tableWidget.resizeColumnsToContents() self.tableWidget.blockSignals(False) + + def onNew(self): + w = Qt.QDialog(self.Form) + w.setWindowTitle('Add New PyAlarm Device') + w.setLayout(Qt.QGridLayout()) + server,device = Qt.QLineEdit(w),Qt.QLineEdit(w) + server.setText('TEST') + device.setText('test/pyalarm/1') + w.layout().addWidget(Qt.QLabel('Server Instance'),0,0,1,1) + w.layout().addWidget(server,0,1,1,1) + w.layout().addWidget(Qt.QLabel('Device Name'),1,0,1,1) + w.layout().addWidget(device,1,1,1,1) + doit = Qt.QPushButton('Apply') + w.layout().addWidget(doit,2,0,2,2) + def create(s=server,d=device,p=w): + try: + s,d = str(s.text()),str(d.text()) + if '/' not in s: s = 'PyAlarm/%s'%s + import fandango.tango as ft + ft.add_new_device(s,'PyAlarm',d) + print('%s - %s: created!'%(s,d)) + except: + traceback.print_exc() + self.api.load() + p.close() + QtCore.QObject.connect(doit, QtCore.SIGNAL("clicked()"), create) + w.exec_() + self.setDevCombo() def onEdit(self): try: @@ -122,4 +161,4 @@ def onEdit(self): ui.devattrchangeSetupUi(Form) Form.show() ui.setDevCombo() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) From 57779fa0692b7dcb6c482ef2dd50d4515155ded7 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 6 Oct 2016 14:37:56 +0200 Subject: [PATCH 39/54] Updating CHANGES with 6.0 targets --- panic/CHANGES | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/panic/CHANGES b/panic/CHANGES index 28ecfc3a..8996c227 100644 --- a/panic/CHANGES +++ b/panic/CHANGES @@ -1,6 +1,10 @@ -ONGOING +ONGOING 6.0 --------------------- + * Alarm Qualities will change to reflect alarm life cycle instead of severity. + * Alarm may be declared in its own class/free property; instead of using Alarm* device properties. + * Alarm collections will be used to manage different sets of alarms in GUI> + Package refactored to build valid system/PIP/rpm packages PyAlarm: @@ -123,4 +127,4 @@ BETA: Cache added to TangoEval to try alarm on transition. Added StartupDelay property Bugs solved in Snap context creation. Added user message to alarm RESET emails. - \ No newline at end of file + From a5516bd7f9751125b1c0de0a77e09ca1eb2cf785 Mon Sep 17 00:00:00 2001 From: srubio Date: Fri, 7 Oct 2016 13:06:24 +0200 Subject: [PATCH 40/54] adding summary of requests --- doc/releases/6.0/requests.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 doc/releases/6.0/requests.txt diff --git a/doc/releases/6.0/requests.txt b/doc/releases/6.0/requests.txt new file mode 100644 index 00000000..e8d0c5a2 --- /dev/null +++ b/doc/releases/6.0/requests.txt @@ -0,0 +1,8 @@ +File to summarize requests by different users: + +F.Becheri: + + - Failed alarms should be easy to spot from panic gui + - Users to be alerted if the PyAlarm is dead or idle + + From d40ae071e1b819841de546fd04ba295e49c144a4 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 16:36:44 +0200 Subject: [PATCH 41/54] Create AlarmDistribution.rst --- doc/releases/6.0/AlarmDistribution.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 doc/releases/6.0/AlarmDistribution.rst diff --git a/doc/releases/6.0/AlarmDistribution.rst b/doc/releases/6.0/AlarmDistribution.rst new file mode 100644 index 00000000..30a10c5a --- /dev/null +++ b/doc/releases/6.0/AlarmDistribution.rst @@ -0,0 +1,21 @@ +Alarms Distribution +=================== + +About distributing load (answer to paul bell, 2014) +--------------------------------------------------- + +We have 1200+ alarms and system works quite well with it. But regarding distribution of PyAlarm devices and servers the rules must be more intelligent. + +Instead of thinking in terms of N attrs/pyalarm you must distribute load trying to group all attributes from the same host or subsystem. + +There are two reasons to do that (and also apply to Archiving): + + - When a host is down you'll have a lot of proxy threads in background trying to reconnect to lost devices. If alarms are distributed on rough numbers it becomes a lot of timeouts spreading through the system. When alarms are grouped by host you isolate the problems. + + - Same applies for very event-intensive devices. Devices that generate a lot of information will need lower attrs/pyalarm ratio than devices that do not change so much. + +Apart of that ... if you have 1000 alarms just for the linac then you may have a wrong specification. I use to say than "all" should be in the order of 10K ; by experience any number about that is too much. If you need more than 10K of a kind what you really need is to add a level of abstraction (do not check all gauges of a vacuum section, just had an attribute where you can read the max value). + +It applies to all Tango systems I've seen (alarms, archiving, save/restore, pool, device tree, ...); if you reach a number above 10K then you must add an abstraction layer. It's not only that you reach a performance limit, also your users will feel too dazed and confused when searching for things. + +e.g. Our accelerator group requested 1200 alarms ... and after some months they asked for a filter to show only the 240 they really care about. From e0b6ec08dfe3edbc17c010047cdcf36815d86230 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 16:37:33 +0200 Subject: [PATCH 42/54] Create AlarmFormulas.rst --- doc/releases/6.0/AlarmFormulas.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 doc/releases/6.0/AlarmFormulas.rst diff --git a/doc/releases/6.0/AlarmFormulas.rst b/doc/releases/6.0/AlarmFormulas.rst new file mode 100644 index 00000000..c8c980af --- /dev/null +++ b/doc/releases/6.0/AlarmFormulas.rst @@ -0,0 +1,25 @@ +Alarm Formulas Examples (Max IV, 2014) +-------------------------------------- + +The proper way is (for readability I use upper case letters for alarms): + + ALARM_1: just/my/tango/attribute_1 + ALARM_2: just/my/tango/attribute_2 + +then: + + ALARM_1_OR_2: ALARM_1 or ALARM_2 + +or: + + ALARM_1_OR_2: any(( ALARM_1 , ALARM_2 )) + +or: + + ALARM_ANY: any( FIND(my/alarm/device/ALARM_*) ) + +Any alarm you declare becomes both a PyAlarm attribute and a variable that you can anywhere (also in other PyAlarm devices). You don't trigger any new read because you just use the result of the formula already evaluated. + +The GROUP is used to tell you that a set of conditions has changed from its previous state. GROUP instead will be triggered not if any is True, but if any of them toggles to True. It forces you to put the whole path to the alarm: + + GROUP(my/alarm/device/ALARM_[12]) From a5c57f13e04277a8a789c8fec8f7b0e152174c1d Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 16:48:11 +0200 Subject: [PATCH 43/54] Update requests.txt --- doc/releases/6.0/requests.txt | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/doc/releases/6.0/requests.txt b/doc/releases/6.0/requests.txt index e8d0c5a2..4ef60bb2 100644 --- a/doc/releases/6.0/requests.txt +++ b/doc/releases/6.0/requests.txt @@ -1,8 +1,31 @@ File to summarize requests by different users: -F.Becheri: +ALBA: - - Failed alarms should be easy to spot from panic gui + - Failed alarms and servers should be easy to spot from panic gui - Users to be alerted if the PyAlarm is dead or idle + - Taurus pop-ups on alarms + +MAXIV: + + - Kibana Integration + - Optimize groups of alarms + - Merge Paul Bell branch: https://sourceforge.net/p/tango-ds/code/HEAD/tree/DeviceClasses/SoftwareSystem/PyAlarm/branches/MAXIV + + SOLEIL: + + - Integration with Vacca + - Integration with the AlarmDB (documents by Katy Saintin). + +Elettra: + + - + +SKA: + + - An statement on PANIC and IEC 62682 compliance + - + + From e9413891249177913e25b276ad77d5a163be1b86 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 16:56:07 +0200 Subject: [PATCH 44/54] Create Exceptions.rst --- doc/releases/6.0/Exceptions.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 doc/releases/6.0/Exceptions.rst diff --git a/doc/releases/6.0/Exceptions.rst b/doc/releases/6.0/Exceptions.rst new file mode 100644 index 00000000..86d31949 --- /dev/null +++ b/doc/releases/6.0/Exceptions.rst @@ -0,0 +1,16 @@ +Alarm properties that control if exceptions trigger alarms or not ... + + 'RethrowState': + [PyTango.DevBoolean, + "Whether exceptions in State reading will be rethrown.", + [ True ] ],#Overriden by panic.DefaultPyAlarmProperties + + 'RethrowAttribute': + [PyTango.DevBoolean, + "Whether exceptions in Attribute reading will be rethrown.", + [ False ] ],#Overriden by panic.DefaultPyAlarmProperties + + 'IgnoreExceptions': + [PyTango.DevBoolean, + "If True unreadable values will be replaced by None instead of Exception.", + [ True ] ],#Overriden by panic.DefaultPyAlarmProperties From fddd04452214c9f09b31cc15cee019a10982cc45 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 17:06:06 +0200 Subject: [PATCH 45/54] Create kibana.rst --- doc/releases/6.0/kibana.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/releases/6.0/kibana.rst diff --git a/doc/releases/6.0/kibana.rst b/doc/releases/6.0/kibana.rst new file mode 100644 index 00000000..81498ada --- /dev/null +++ b/doc/releases/6.0/kibana.rst @@ -0,0 +1,7 @@ +http://www.tango-controls.org/community/forum/post/1123/ + +we've added a tiny feature to PyAlarm which pushes each alarm event as a JSON document to a simple "logger" device (using a command), which in turn stores the event in elasticsearch. The historical data can then be viewed through the kibana web UI, where users can do various filtering and also set up specific views. So far it has been pretty solid, with very low maintenance. + +I'm attaching a kibana screenshot from our controlroom. The UI is a bit strange but powerful once you get used to it. However, the main benefit is that we're not developing it ourselves :) + +Caveat: we're currently using ES 1.X and kibana 3, but the current version of ES is 2.X and kibana 3 is no longer compatible. Kibana 4 is a complete rewrite and works quite differently, with an even more confusing UI. We're not sure whether to migrate or how. From bb18ca82108a68cee88f3845d51d21460769f3e4 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 17:06:40 +0200 Subject: [PATCH 46/54] Create AlarmProperties.rst --- doc/releases/6.0/AlarmProperties.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 doc/releases/6.0/AlarmProperties.rst diff --git a/doc/releases/6.0/AlarmProperties.rst b/doc/releases/6.0/AlarmProperties.rst new file mode 100644 index 00000000..6f418ba9 --- /dev/null +++ b/doc/releases/6.0/AlarmProperties.rst @@ -0,0 +1,15 @@ +* StartupDelay: the device will wait before starting to evaluate the alarms (e.g. giving some time to the system to recover from a powercut). + +* Enabled: if False or 0 the PyAlarm it equals to disabling all alarm actions of the device; if it is True the behavior will be the normal expected; if it has a numeric value (e.g. 120) it means that the device will evaluate the alarms but not execute actions during the first 120 seconds (thus alarms can be activated but no action executed). It is used to prevent a restart of the device to re-execute all alarms that were already active. + +* EvalTimeout: The proxy timeout used when evaluating the attributes (any read attribute slower than timeout will raise exception). + +* AlarmThreshold: number of cycles that an alarm must evaluate to True to be considered active (to avoid alarms on "glitches"). + +* RethrowAttribute/RethrowState: Whether exceptions on reading attributes or states should be rethrown to higher levels, thus causing the alarm to be triggered. By default alarms are enabled if an State attribute is not readable (RethrowState=True), but when a numeric attribute is not readable its value is just replaced by None (RethowAttribute=False) and the formula evaluated normally. + +* Reminder: A new email will be sent every XX seconds if the alarm remains active. When AlertOnRecovery is True an email will be sent also every time when the formula result oscillates from True to False. + +* UseProcess: This is an experimental feature, like UseTaurus and others. In general, I advice you to not modify any parameter that is not detailed in the PyAlarm user guide as you may obtain unexpected results. Some parameters are used to test new features still under development and their behavior may vary between commits. + +Regarding actions on recovery … this option is planned but not yet fully available. Actually just emails are sent when AlertOnRecovery is True. This feature may be implemented in the next 6 months or so but the syntax is still to be decided. From f0031535a6d7fa1ea9e2c1178583c4e13a3c67b7 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 17:19:22 +0200 Subject: [PATCH 47/54] Update kibana.rst --- doc/releases/6.0/kibana.rst | 121 ++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/doc/releases/6.0/kibana.rst b/doc/releases/6.0/kibana.rst index 81498ada..e41c8b85 100644 --- a/doc/releases/6.0/kibana.rst +++ b/doc/releases/6.0/kibana.rst @@ -5,3 +5,124 @@ we've added a tiny feature to PyAlarm which pushes each alarm event as a JSON do I'm attaching a kibana screenshot from our controlroom. The UI is a bit strange but powerful once you get used to it. However, the main benefit is that we're not developing it ourselves :) Caveat: we're currently using ES 1.X and kibana 3, but the current version of ES is 2.X and kibana 3 is no longer compatible. Kibana 4 is a complete rewrite and works quite differently, with an even more confusing UI. We're not sure whether to migrate or how. + +---- + +PyAlarm Elasticsearch logging +----------------------------- + +The purpose of these changes is to enable PyAlarm to push all alarm events into a database, in this case Elasticsearch but it should be pretty easy to support other databases if desired. The main reason we're going with ES is that it is already established as a platform for storing logs, and has some mature UI tools for this such as Kibana. + +I've tried to compile the changes we've made to PyAlarm to support logging to elasticsearch. The system has been in use for about 1 year now and has worked pretty well. The solution also requires another part, namely the "logger" device. It is a separate device that has an "Alarm" command that takes a JSON string and stores that as a document in elasticsearch. This device is currently undergoing some work, mainly to support newer versions of ES, but we will make it available soon. + +This is not a patch, since I think we've diverged from the main branch of PyAlarm. We tried to make the changes as lcal as we could so it should be easy to just put it in. You can of course refactor this as you like if you think it would fit better in some other way. + +We added an optional string property called "LoggerDevice". It can be configured with the name of a "logger" device. If a logger device is configured, we try to create a proxy to it in "init_device", and if this is successdul, save it in an internal variable called "self._loggerds". If the property is not set, the PyAlarm behavior is not affected. + +Then, there are a few additions to the "send_alarm" method in PyAlarm: +---- +if self._loggerds: + # send alarm data to the logger device + report = self.generate_json(tag_name, message=message, values=values) + self._loggerds.alarm(report) +---- +and "free_alarm" method: +---- +if self._loggerds: + # send alarm data to the logger device + report = self.generate_json(tag_name, message=message, user_comment=comment) + self._loggerds.alarm(report) +---- + +(Note: The communication with the logger device should perhaps be done in an asynchronous way, so that it won't block PyAlarm if the logger device is slow.) + +They call the following method that was added to PyAlarm: + +---- +def generate_json(self, tag_name, message='DETAILS', + values=None, user_comment=None, html=False): + """ + Take an alarm and turn it into a JSON string representation. + The format of this string is dictated by the Logger + device, and follows the shape of the Alarm object. + """ + + # Check alarm + try: + msg = "Generating a json report for alarm {0}" + self.info(msg.format(tag_name)) + alarm = self.Alarms[tag_name] + except KeyError: + return self.warn('Unknown alarm: {0}'.format(tag_name)) + + # Helper function + def cast_dict(dct): + """Convert Boost.Enum objects to strings""" + boost_eval_type = PyTango._PyTango.AttrQuality.__base__ + for key, value in dct.items(): + if isinstance(value, boost_eval_type): + dct[key] = str(value) + + # Build dictionary + try: + self.info("Building dictionary for alarm {0}".format(tag_name)) + _values = values or self.PastValues.get(tag_name) or {} + cast_dict(_values) # Convert Boost.Enum objects to string + report = { + "timestamp": int(time.time() * 1000), + "alarm_tag": tag_name, + "message": message.strip(), + "values": [{"attribute": attr, "value": value} + for attr, value in _values.items()], + "device": self.get_name().strip(), + "description": alarm.parse_description().strip(), + "severity": alarm.parse_severity().strip(), + "instance": alarm.instance, + "formula": alarm.formula.strip() + } + if user_comment: + report["user_comment"] = user_comment + if alarm.recovered: + report["recovered_at"] = int(alarm.recovered * 1000) + if alarm.active: + report["active_since"] = int(alarm.active * 1000) + except Exception as exc: + msg = 'Unexpected exception while building dictionary' + msg = 'for alarm {0}: '.format(tag_name) + return self.warn(msg + repr(exc)) + + # Dump the json string + try: + self.info("Dumping json for alarm {0}".format(tag_name)) + string = json.dumps(report) + except Exception as exc: + msg = 'Unexpected exception while dumping json' + msg = 'for alarm {0}: '.format(tag_name) + return self.warn(msg + repr(exc)) + else: + self.debug(string.replace('%', '%%')) + + # Return json + return string +---- + +Finally, in order for each alarm "event" to be identifiable in the DB, we added an "instance" field to the Alarm object that is a unique ID that ties each activation and deactivation of an alarm together. This is not really necessary for the operation, but it may turn out to be useful in the future. The point is that if you have the activation event of an alarm it makes it easy to find when it was deactivated, and vice versa. + +In Alarm we added the method: +---- + def activate(self): + self.active = time.time() + self.instance = str(uuid4()) # import uuid4 from uuid +---- +And then in PyAlarm replaced the line: +---- + self.Alarms[tag_name].active = time.time() +---- +with +---- + self.Alarms[tag_name].activate() +---- + +That's it! + +/Johan From 829cac20ce135fc0fc3b29c7e55f089774cde070 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Thu, 13 Oct 2016 17:42:07 +0200 Subject: [PATCH 48/54] Update requests.txt --- doc/releases/6.0/requests.txt | 190 ++++++++++++++++++++++++++++++++-- 1 file changed, 179 insertions(+), 11 deletions(-) diff --git a/doc/releases/6.0/requests.txt b/doc/releases/6.0/requests.txt index 4ef60bb2..eca9bc25 100644 --- a/doc/releases/6.0/requests.txt +++ b/doc/releases/6.0/requests.txt @@ -1,31 +1,199 @@ File to summarize requests by different users: -ALBA: +ALBA +---- - Failed alarms and servers should be easy to spot from panic gui - Users to be alerted if the PyAlarm is dead or idle - Taurus pop-ups on alarms + - set alarms when the readback is different from the last user input in a given attribute. + - protect againts using "is" in formulas; should be replaced by "==" + - add a command to get last traceback stack in case of Failed alarm. + - persistent disable after restart (via Enabled property!?) + - asynchronous alarm evaluation from Panic GUI preview + - + +ESRF +---- + + - Guide to deploy PANIC/PyAlarm on an empty Debian system -MAXIV: +MAXIV +----- - Kibana Integration - - Optimize groups of alarms - - Merge Paul Bell branch: https://sourceforge.net/p/tango-ds/code/HEAD/tree/DeviceClasses/SoftwareSystem/PyAlarm/branches/MAXIV + - Optimize groups of alarms against host-down exceptions. + - Merge Paul Bell & Johan Forsberg branches: (PRJ/Alerts/branches/maxiv/app-dev-pyalarm.tgz) - SOLEIL: + SOLEIL + ------ - Integration with Vacca - - Integration with the AlarmDB (documents by Katy Saintin). + - Integration with the AlarmDB (see document by Katy Saintin). -Elettra: +TCS +--- + + - Implement multi-tango_host in all queries / formulas. Multiple-host alarm support is one of the important requirement of project. + - Is script execution possible on alarm action ? ACTION(script,path,args) (It should be accompanied of an AllowScripts property and a list of valid script receptors; to prevent a harmful usage of this kind of tool). + - >1500 alarms performance issues (see issues log at the end) + + - Is there any way to write the rule by reading attribute name and device name from custom database ? It means that in defining above rule, I don't have to manually write rule for all parameters. It will read the device and attribute name from database and write the rule accordingly. ===> SEE ALIASES + - ALIASES: The rule must also be configurable, Suppose if Speed1 becomes Speed111, then rule will also be l/m/n/Speed111.quality==ATTR +_ALARM + - set action if alarms recovers in AlarmReceiver. RECOVER(command,...) + - set the action to set the alarm status as "Alarm reset". RECOVER(attribute,a/b/c/alarmstatus,'All Ok') + + - GUI: I have around 150 alarms in my list. When I try to disable single/multiple alarms, its not disabling the selected alarms but when the alarm number is less, then I am able to perform the disable action. Same problem is with acknowledging single/multiple alarms. + + - GUI+CSV: I am trying to import from .csv file. It has around 150 alarms, so the window which opens which says"Choose alarms to import" doesn't have scrollbar attached and I am unable to browse through the lower alarms. Similarly, the problem is with disable and delete button. If you try to delete and disable around 150 alarms, the scrollbar doesnt come. So you couldnt see ok button. I have attached the screenshot of the same problem + + - ACTION(alarm:command, LMC1:10000/lmc/c01/gab/systemCmd,1,2,3). But it is not working. + - command when alarm occurs without providing arguments is not working. + - Multiple-host snap is not working. This is very important requirement for me. But Panic doesnt support getting snaps from multiple-hosts. + - Does Panic support user specific login ? As of now anyone can open the Panic-Gui, but need user login here. Can it be done ? Also have to disable the rule editing/acknowledgement/disable/enable button for some specific user. It means that some user have read only permission and some will have both read-write permission? Can it be done user specific ? + - If Alarm comes, can i snooze it for some time ? + +1.) When alarm comes at CMC, it should archive it in CMC Snap database. + Suppose in one rule around 1350 attributes conditions have been written like this type: + +LMC1:10000/LMC/C01/GAB/Ch01.quality==ATTR_ALARM or LMC2:10000/LMC/C02/GAB/Ch01.quality==ATTR_ALARM or LMC3:10000/LMC/C03/GAB/Ch01.quality==ATTR_ALARM or LMC4:10000/LMC/C04/GAB/Ch01.quality==ATTR_ALARM + +If alarm occurs due to 1 attribute, can that attribute name and value be stored in snap database ? + +2.) When one or two LMC's/ TangoHost are not available, CMC Panic shouldn't slow down. Presently it is happening. + Currently I have defined the condition in CMC for 3 LMC i.e for around 1350 attributes in one rule, it is getting very slow and also gets closed due to segmentation fault. +System shouldn't slow down if any of the device is unreachable. + +3.) Writing condition for 1350 attribute in 1 rules is very tedious, It would be helpful if the grouping works. I will send you the result of the code you sent once we completely migrate to Ubuntu. + +1.) Panic GUI becomes very slow when LMC gets disconnected. There are performance issues. +2.) Is folder-ds released, which would help in logging alarm for multiple hosts as per your below mail +3.) The wild card pattern sometimes doesn't support. I am using it for as many as 1200 attributes. +4.) Is user authentication supported in Panic ? + +Actually i need Panic-GUI login regardless of OS User Login. There will be set of users, for them there will be specific permissions, such as some user would be able to acknowledge the alarm, some won't, some users should be able to edit the rule, some wont have permission to do so. Also I need User log, like, who has acknowledged the alarms, who has added the new alarm, who has edited the rule, i.e. every user activity to be logged. Is this possible ? + - - -SKA: +Elettra +------- - - An statement on PANIC and IEC 62682 compliance - - + - Common API/Specification for Tango properties usage. + - Having the same Alarm in 2 different Pyalarm instances !?!? (G.Scalamera) + - Notify opened clients when there are changes in an alarm configuration. + +SKA/INAF +-------- + + - An statement on PANIC and IEC 62682 compliance (e.g. incorporating PANIC as redundant level) + - Install fandango/PANIC via PIP (Neilen Marais) + - Fast Alarms triggered by single attribute events. +---- + +PERFORMANCE ISSUES +------------------ + +
+I have one Central-Monitoring-Control (cmc:10000) and three lmc's which are LMC1 (LMC1:10000), LMC2(LMC2:10000), LMC3(LMC3:10000).
+
+LMC1 have 5 devices such as: LMC/C01/GAB, LMC/C01/FPS, LMC/C01/FECB, LMC/C01/SERVO, LMC/C01/OFCSNT)
+LMC2 have 5 devices such as: LMC/C02/GAB, LMC/C02/FPS, LMC/C02/FECB, LMC/C02/SERVO, LMC/C02/OFCSNT)
+LMC3 have 5 devices such as: LMC/C03/GAB, LMC/C03/FPS, LMC/C03/FECB, LMC/C03/SERVO, LMC/C03/OFCSNT)
+Each device have around 70 attributes.
+So one LMC have around 70*5=350 attributes.
+
+Now I have to define a rule for all the attributes of LMCs in CMC. I tried the easier way by grouping.
+
+I tried to define rule as:
+
+any([t==ATTR_ALARM for t in FIND(LMC1:10000/LMC/C01/*/*.quality)]) or any([t==ATTR_ALARM for t in FIND(LMC2:10000/LMC/C02/*/*.quality)]) or any([t==ATTR_ALARM for t in FIND(LMC3:10000/LMC/C03/*/*.quality)])
+
+But the result is:
+
+test/alarms/1: DevFailed[
+
+DevError[
+
+desc = TRANSIENT CORBA system exception: TRANSIENT_CallTimedout
+
+origin = Connection::command_inout()
+
+reason = API_CorbaException
+
+severity = ERR]
+
+
+DevError[
+
+desc = Timeout (500 mS) exceeded on device test/alarms/1, command evaluateFormula
+
+origin = Connection::command_inout()
+
+reason = API_DeviceTimedOut
+
+severity = ERR]
+
+]
+
+Then I tried the long way which is individually defining the rule for all the attributes of all LMCs.
+
+For example, below is the rule:
+
+LMC1:10000/LMC/C01/GAB/ch1.quality==ATTR_ALARM or
+LMC2:10000/LMC/C02/GAB/ch1.quality==ATTR_ALARM or
+LMC3:10000/LMC/C03/GAB/ch1.quality==ATTR_ALARM or
+LMC1:10000/LMC/C01/GAB/ch2.quality==ATTR_ALARM or
+LMC2:10000/LMC/C02/GAB/ch2.quality==ATTR_ALARM or
+LMC3:10000/LMC/C03/GAB/ch2.quality==ATTR_ALARM or
+LMC1:10000/LMC/C01/GAB/ch3.quality==ATTR_ALARM or
+LMC2:10000/LMC/C02/GAB/ch3.quality==ATTR_ALARM or
+LMC3:10000/LMC/C03/GAB/ch3.quality==ATTR_ALARM
+
+Like the above I typed for around all LMCs attributes 750 times which was very tedious.
+
+and the result is same as above which is:
+
+test/alarms/1: DevFailed[
+
+DevError[
+
+desc = TRANSIENT CORBA system exception: TRANSIENT_CallTimedout
+
+origin = Connection::command_inout()
+
+reason = API_CorbaException
+
+severity = ERR]
+
+
+DevError[
+
+desc = Timeout (500 mS) exceeded on device test/alarms/1, command evaluateFormula
+
+origin = Connection::command_inout()
+
+reason = API_DeviceTimedOut
+
+severity = ERR]
+
+]
+
+Because of the above error, rule becomes true and is in alarm condition even if it is not.
+
+Ideally it shouldn't happen, because my future requirement is like , I
+would be having 30 LMCs, so it would be very tedious job to configure the
+rules for all the lmcs unless grouping works.
+
+Secondly, the GUI-tool has become very slow and hangs and then gets closed and provides segmentation fault. Also the alarm device server is using too much memory, which slows down the system.
+
+Thirdly, Can i Take action from CMC to any LMC command or LMC attribute ? Below is the syntax i tried:
+
+ACTION(alarm:command, LMC1:10000/lmc/c01/gab/systemCmd,1,2,3). But it is not working.
 
+Fourth, Executing command when alarm occurs without providing arguments is not working.
 
+Fifth, Multiple-host snap  is not working. This is very important requirement for me. But Panic doesnt support getting snaps from multiple-hosts.
+
From 0ea4f6e9f4c46394e3399acd27c44867be88db88 Mon Sep 17 00:00:00 2001 From: srubio Date: Sun, 16 Oct 2016 07:53:15 +0200 Subject: [PATCH 49/54] Enable to send logs remotely via FolderDS device --- panic/ds/PyAlarm.py | 77 +++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/panic/ds/PyAlarm.py b/panic/ds/PyAlarm.py index 46b1e2d7..7ed348a9 100644 --- a/panic/ds/PyAlarm.py +++ b/panic/ds/PyAlarm.py @@ -484,7 +484,9 @@ def updateAlarms(self): self.set_alarm(tag_name) self.LastAlarms.append(time.ctime(now)+': '+tag_name) self.PastValues[tag_name] = variables.copy() if hasattr(variables,'copy') else None #Storing the values that will be sent in the report - if alarm.tag not in self.AcknowledgedAlarms: self.send_alarm(tag_name,message='ALARM',values=variables or None) # <=== HERE IS WHERE THE ALARM IS SENT! + if alarm.tag not in self.AcknowledgedAlarms: + # <=== HERE IS WHERE THE ALARM IS SENT! + self.send_alarm(tag_name,message='ALARM',values=variables or None) # Sending REMINDER ######################################################## @@ -544,7 +546,6 @@ def set_alarm(self,tag_name): self.Alarms[tag_name].recovered = 0 result = True self.update_flag_file() - self.update_log_file() finally: self.lock.release() return result @@ -579,11 +580,14 @@ def free_alarm(self,tag_name,comment='',message=None, notify=True): self.lock.release() try: #NOTIFICATION SHOULD NOT BE WITHIN THE LOCK if was_active: + mail_receivers = self.parse_receivers(tag_name,'@',receivers) + report = self.GenerateReport(tag_name,mail_receivers,message=message or 'RESET',user_comment=comment) + if self.get_enabled() and notify: print '\treceivers: %s'%receivers - ## Actions must be evaluated first in case rapid action is needed action_receivers = self.parse_action_receivers(tag_name,message,receivers) + if action_receivers: self.info('checking %s actions ... %s'%(message,action_receivers)) for ac in action_receivers: @@ -591,14 +595,12 @@ def free_alarm(self,tag_name,comment='',message=None, notify=True): self.trigger_action(alarm_obj,ac,message) except: self.warning( 'PyAlarm.trigger_action crashed with exception:\n%s' % traceback.format_exc()) - try: - mail_receivers = self.parse_receivers(tag_name,'@',receivers) if mail_receivers: - self.SendMail(self.GenerateReport(tag_name,mail_receivers,message=message or 'RESET',user_comment=comment)) + self.SendMail(report) except Exception,e: self.warning( 'PyAlarm.SendMail crashed with exception:\n%s' % traceback.format_exc()) - + try: if SNAP_ALLOWED and (self.UseSnap or self.parse_receivers(tag_name,'SNAP',receivers)): self.info('>'*80+'\n'+'triggering snapshot for alarm:'+tag_name) @@ -612,9 +614,11 @@ def free_alarm(self,tag_name,comment='',message=None, notify=True): self.SaveHtml(self.GenerateReport(tag_name,message=message or 'RESET',user_comment=comment, html=True)) except Exception, e: self.warning( 'PyAlarm.saveHtml crashed with exception:\n%s' % traceback.format_exc()) - + + if self.LogFile: self.update_log_file(tag=tag_name,report=report) + self.update_flag_file() - self.update_log_file() + except: self.warning( 'ResetAlarm(%s) failed!: %s' % (tag_name,traceback.format_exc())) self.info('-'*80) @@ -653,6 +657,8 @@ def send_alarm(self,tag_name,message='',values=None): finally: self.lock.release() + report = self.GenerateReport(tag_name,mail_receivers,message=message,values=values) + if self.get_enabled(): if self.Alarms[tag_name].severity=='DEBUG': self.info('%s Alarm with severity==DEBUG do not trigger actions, messages or snapshot'%tag_name) @@ -667,7 +673,7 @@ def send_alarm(self,tag_name,message='',values=None): self.warning( 'PyAlarm.trigger_action crashed with exception:\n%s' % traceback.format_exc()) if mail_receivers: try: - self.SendMail(self.GenerateReport(tag_name,mail_receivers,message=message,values=values)) + self.SendMail(report) except: self.debug('-'*80) self.warning('Exception sending email!: %s' % traceback.format_exc()) @@ -695,6 +701,7 @@ def send_alarm(self,tag_name,message='',values=None): else: self.info('=============> ALARM SENDING DISABLED!!') + self.update_log_file(tag=tag_name,report=report) self.Alarms[tag_name].sent += 1 self.Alarms[tag_name].last_sent = time.time() except Exception,e: @@ -716,21 +723,39 @@ def update_flag_file(self): finally: self.lock.release() - def update_log_file(self): - if not hasattr(self,'LogFile') or not self.LogFile or self.LogFile.strip()=='/dev/null': return + def update_log_file(self,argin='',tag='',report=''): + logfile = (argin.strip() or self.LogFile or '').strip() + if not logfile or logfile == '/dev/null': return try: self.lock.acquire() - f=open(self.LogFile,'a') - report = [] - report.append('%s PyAlarm Device Server at %s\n\n' % (self.get_name(),time.ctime())) - if self.get_active_alarms(): - report.append('Active Alarms are:\n') - [report.append('\t%s:%s:%s\n'%(k,time.ctime(self.Alarms[k].active),self.Alarms[k].formula)) for k in self.get_active_alarms()] - else: report.append( "There's No Active Alarms\n") - if self.PastAlarms: - report.append( '\n\n' + 'Past Alarms were:' + '\n\t'.join([''] + ['%s:%s'%(','.join(k),time.ctime(d)) for d,k in self.PastAlarms.items()]) +'\n') - f.writelines(report) - f.close() + date = fandango.time2str().replace('-','').replace(':','').replace(' ','') + device = self.get_name().replace('/','_').replace('-','_').upper() + logfile = logfile.replace('$DEVICE',device).replace('$NAME',tag).replace('$DATE',date).replace('$ALARM',tag) + + if not report: + report = [] + report.append('%s PyAlarm Device Server at %s\n\n' % (self.get_name(),time.ctime())) + if self.get_active_alarms(): + report.append('Active Alarms are:\n') + [report.append('\t%s:%s:%s\n'%(k,time.ctime(self.Alarms[k].active),self.Alarms[k].formula)) for k in self.get_active_alarms()] + else: report.append( "There's No Active Alarms\n") + if self.PastAlarms: + report.append( '\n\n' + 'Past Alarms were:' + '\n\t'.join([''] + ['%s:%s'%(','.join(k),time.ctime(d)) for d,k in self.PastAlarms.items()]) +'\n') + + if fun.isSequence(report): + report = '\n'.join(report) + + if fun.clmatch('(folderds|tango)[:].*',logfile): + self.info('Sending alarm log to %s'%logfile) + try: + fandango.device.FolderAPI().save(logfile,logfile,report,asynch=True) + except: + self.warning(traceback.format_exc()) + else: + self.info('Saving alarm values to %s'%logfile) + f=open(logfile,'a') + f.write(report) + f.close() except Exception,e: self.warning( 'Exception in PyAlarm.update_log_file: %s' % traceback.format_exc()) finally: @@ -1075,8 +1100,6 @@ def init_device(self,update_properties=True,allow=True): self.info('Configured WorkerProcess, waiting %s seconds in background ...'%self.StartupDelay) self.PhoneBook = self.Alarms.phonebook - if '$NAME' in self.LogFile: - self.LogFile = self.LogFile.replace('$NAME',self.get_name().replace('/','-')) self.AddressList = dict(self.PhoneBook) @@ -1746,8 +1769,8 @@ def format4html(report): return out try: report=argin[1]+'\n'+argin[0] - filename=argin[1].split(' ', 2)[1]+'.html' - f=open(self.HtmlFolder+'/'+filename, 'w') + logfile=argin[1].split(' ', 2)[1]+'.html' + f=open(self.HtmlFolder+'/'+logfile, 'w') report = format4html(report) f.write(report) f.close() From 5f285fdb1a71987842d6a6dc63bf592f7f141c1d Mon Sep 17 00:00:00 2001 From: sergirubio Date: Sun, 16 Oct 2016 08:25:04 +0200 Subject: [PATCH 50/54] Create Install.rst --- doc/Install.rst | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 doc/Install.rst diff --git a/doc/Install.rst b/doc/Install.rst new file mode 100644 index 00000000..6a5f908a --- /dev/null +++ b/doc/Install.rst @@ -0,0 +1,45 @@ +Installing PANIC on a New System +================================ + +Dependencies +------------ + +PANIC is available from Github, PyPI and as Debian or SuSE packages. + +If you install from SuSE or Debian packages dependencies will be automatically installed. + +If not, then you'll need Tango, PyTango and Fandango for the server side (including its dependencies, ZMQ, numpy, ...). + +For the client side you'll also need Taurus library and PyQt4. + +You should be able to get all these packages also from www.tango-controls.org + +Run the GUI and create a PyAlarm +-------------------------------- + +Running "setup.py install" should install the panic-gui script in your system. + +But if you don't want to install the application you can just run python panic/gui/gui.py to launch the client. + +In your first run it will apply completely empty. Just create your first PyAlarm instance going to the "Config" icon in the toolbar and pushing "Create New" button. + +Now you can create your first PyAlarm pushing "New" in the main widget. You'll be prompted to fill the gaps, for a first installation I recommend this alarm: + + TAG: TEST_LOG + Description: just testing + Severity: WARNING + Receivers: your_mail@your_domain.com + Formula: True + +This simple alarm will allow you to check if email sending works properly. + +Run the PyAlarm Server +---------------------- + +Use Astor or the shell to start your newly created PyAlarm: + + python ds/PyAlarm.py TEST -v4 + +After ~45 seconds (if you didn't modified the default configuration) you'll receive your first email from PANIC. + +Now head to the configuration docs to know all the options you have for tuning the behaviour. From 645d8699944a96f4c6f84bfdf41ea1011b34bf17 Mon Sep 17 00:00:00 2001 From: sergirubio Date: Tue, 18 Oct 2016 16:24:03 +0200 Subject: [PATCH 51/54] Create Testing.rst --- doc/releases/6.0/Testing.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 doc/releases/6.0/Testing.rst diff --git a/doc/releases/6.0/Testing.rst b/doc/releases/6.0/Testing.rst new file mode 100644 index 00000000..95b984e5 --- /dev/null +++ b/doc/releases/6.0/Testing.rst @@ -0,0 +1,3 @@ +Testing Recipes +=============== + From 4b66064e72eaed0f568780c8e561a2d9d115ee6b Mon Sep 17 00:00:00 2001 From: srubio Date: Tue, 18 Oct 2016 17:20:55 +0200 Subject: [PATCH 52/54] update LogFile property description --- panic/panic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/panic/panic.py b/panic/panic.py index 6d6a24bf..22fa5fb0 100644 --- a/panic/panic.py +++ b/panic/panic.py @@ -145,7 +145,10 @@ ALARM_LOGS = { 'LogFile': [PyTango.DevString, - "File where alarms are logged, like /tmp/alarm_$NAME.log", + "File where alarms are logged, like /tmp/alarm_$NAME.log\n + Keywords are $DEVICE,$ALARM,$NAME,$DATE\n + From version 6.0 a FolderDS-like device can be used for remote logging:\n + \ttango://test/folder/01/$ALARM_$DATE.log", [ "" ] ], 'HtmlFolder': [PyTango.DevString, From 129fda92fb22598d9aa1478f7df8031ff0a11064 Mon Sep 17 00:00:00 2001 From: Sergio Rubio Manrique Date: Tue, 18 Oct 2016 17:25:46 +0200 Subject: [PATCH 53/54] Set state/quality to FAULT/INVALID when alarms fail --- panic/ds/PyAlarm.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/panic/ds/PyAlarm.py b/panic/ds/PyAlarm.py index 7ed348a9..6101f73b 100644 --- a/panic/ds/PyAlarm.py +++ b/panic/ds/PyAlarm.py @@ -12,7 +12,7 @@ # # project : TANGO Device Server # -# $Author: sergi_rubio at cells$ +# $Author: srubio at cells$ # # $Revision: $ # @@ -183,7 +183,9 @@ def alarm_attr_read(self,attr,fire_event=True): self.debug('PyAlarm(%s).read_alarm_attribute(%s) is %s; Active Alarms: %s' % (self.get_name(),tag_name,value,self.get_active_alarms())) self.quality=PyTango.AttrQuality.ATTR_WARNING - if(self.Alarms[tag_name].severity=='DEBUG'): + if tag_name in self.FailedAlarms or self.CheckDisabled(tag_name): + self.quality=PyTango.AttrQuality.ATTR_INVALID + elif(self.Alarms[tag_name].severity=='DEBUG'): self.quality=PyTango.AttrQuality.ATTR_VALID elif(self.Alarms[tag_name].severity=='WARNING'): self.quality=PyTango.AttrQuality.ATTR_WARNING @@ -1152,6 +1154,8 @@ def always_executed_hook(self): status+='%s:%s:\n\t%s\n\tSeverity:%s\n\tSent to:%s\n' % (time.ctime(date),tag_name,self.Alarms[tag_name].description,self.Alarms[tag_name].severity,self.Alarms[tag_name].receivers) if self.FailedAlarms: status+='\n%d alarms couldnt be evaluated:\n%s'%(len(self.FailedAlarms),','.join(str(t) for t in self.FailedAlarms.items())) + if float(len(self.FailedAlarms))/len(self.Alarms) > 0.1: + self.set_state(PyTango.DevState.FAULT) if self.Uncatched: status+='\nUncatched exceptions:\n%s'%self.Uncatched status += '\n\n' + 'EvalTimes are: \n %s\n'%(self.EvalTimes) From 071b37fbdbbfd8ca5d0f69166f5cf3b45e316f50 Mon Sep 17 00:00:00 2001 From: Sergio Rubio Manrique Date: Tue, 18 Oct 2016 17:42:31 +0200 Subject: [PATCH 54/54] Fix bug on LogFile description, add LogFile test --- panic/panic.py | 4 +- panic/test/test_logfile.jive | 130 +++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 panic/test/test_logfile.jive diff --git a/panic/panic.py b/panic/panic.py index 22fa5fb0..0d41161f 100644 --- a/panic/panic.py +++ b/panic/panic.py @@ -145,10 +145,10 @@ ALARM_LOGS = { 'LogFile': [PyTango.DevString, - "File where alarms are logged, like /tmp/alarm_$NAME.log\n + """File where alarms are logged, like /tmp/alarm_$NAME.log\n Keywords are $DEVICE,$ALARM,$NAME,$DATE\n From version 6.0 a FolderDS-like device can be used for remote logging:\n - \ttango://test/folder/01/$ALARM_$DATE.log", + \ttango://test/folder/01/$ALARM_$DATE.log""", [ "" ] ], 'HtmlFolder': [PyTango.DevString, diff --git a/panic/test/test_logfile.jive b/panic/test/test_logfile.jive new file mode 100644 index 00000000..fbb786ab --- /dev/null +++ b/panic/test/test_logfile.jive @@ -0,0 +1,130 @@ +# +# Resource backup , created Tue Oct 18 17:41:31 CEST 2016 +# + +#--------------------------------------------------------- +# SERVER PyAlarm/test, DDebug device declaration +#--------------------------------------------------------- + +PyAlarm/test/DEVICE/DDebug: "sys/DDebug/PyAlarm_Test" + + +#--------------------------------------------------------- +# CLASS DDebug properties +#--------------------------------------------------------- + + +#--------------------------------------------------------- +# SERVER PyAlarm/test, PyAlarm device declaration +#--------------------------------------------------------- + +PyAlarm/test/DEVICE/PyAlarm: "test/alarms/alarms-sim",\ + "test/pyalarm/logfile" + + +# --- test/alarms/alarms-sim properties + +test/alarms/alarms-sim->AlertOnRecovery: True +test/alarms/alarms-sim->AutoReset: 60 +test/alarms/alarms-sim->CreateNewContexts: True +test/alarms/alarms-sim->PollingPeriod: 5 +test/alarms/alarms-sim->__SubDevices: "sys/database/2",\ + "archiving/snapmanager/1",\ + "archiving/snaparchiver/1",\ + "archiving/snapextractor/1" + +# --- test/pyalarm/logfile properties + +test/pyalarm/logfile->AlarmList: TEST:True +test/pyalarm/logfile->AlarmReceivers: srubio@cells.es +test/pyalarm/logfile->AlarmThreshold: 3 +test/pyalarm/logfile->Enabled: 5 +test/pyalarm/logfile->LogFile: "tango://controls02:10000/test/folder/tmp-folderds/$ALARM_$DATE.log" +test/pyalarm/logfile->PollingPeriod: 3 +test/pyalarm/logfile->StartupDelay: 5 + +#--------------------------------------------------------- +# CLASS PyAlarm properties +#--------------------------------------------------------- + +CLASS/PyAlarm->PanicAdminUsers: "" +CLASS/PyAlarm->Phonebook: #CONTROLS,\ + "%FLOOR:floorcoordinators@cells.es,SMS:+34608018721",\ + "",\ + "%CTRLMV:oncall@cells.es,SMS:+34669264304",\ + "%CTRL2:oncall@cells.es,SMS:+34638420276",\ + "%CONTROL:oncall@cells.es,SMS:+34669264304",\ + %CTRLEPS:SMS:+34638420276,\ + "%ANTONIO:amilan@cells.es,SMS:+34680885870",\ + "%SRUBIO:srubio@cells.es,SMS:+34606330920",\ + "%LOTHAR:lkrause@cells.es,SMS:+34606864961",\ + "%LKRAUSE:lkrause@cells.es,SMS:+34606864961",\ + "%MAREK:mgrabski@cells.es,SMS:+34693358058",\ + "%FULVIO:fbecheri@cells.es,SMS:+34649283471",\ + "%ZBIGNIEW:zreszela@cells.es,SMS:+34638404618",\ + "%DFERNANDEZ:dfernandez@cells.es,SMS:+34647649053",\ + %SBLANCH:sblanch@cells.es,\ + %JVILLANUEVA:jvillanueva@cells.es,\ + %MANOLO:mbroseta@cells.es,\ + %TEST:%TEST2,\ + %TEST1:SMS:+34606330920,\ + %TEST2:srubio@cells.es,\ + "",\ + "%SENDMAIL:ACTION(alarm:command,lab/ct/alarms/SendMail,$DESCRIPTION,$ALARM,srubio@cells.es)",\ + "",\ + #VACUUM,\ + "%VACUUM:vacuum@cells.es,SMS:+34669267453,SMS:+34646260812",\ + "%VACMV:vacuum@cells.es,SMS:+34669267453,SMS:+34646260812",\ + "%LLUIS:lgines@cells.es,SMS:+34657045092",\ + "%LGINES:lgines@cells.es,SMS:+34648465913",\ + "%NOESHRAQ:eshraq@cells.es,SMS:+34699282557",\ + %ROGER:rfos@cells.es,\ + "%MAREK:mgrabski@cells.es,SMS:+34693358058",\ + "%JNAVARRO:jnavarro@cells.es,SMS:+34670247608",\ + "%AURELIEN:alacroix@cells.es,SMS:+33623872876",\ + "",\ + #FRONTENDS:,\ + "%JMARCOS:jmarcos@cells.es,SMS:+34670825949",\ + "%JOEL:jpasquaud@cells.es,SMS:+34651436676",\ + "",\ + #BEAMLINES:,\ + %ABARLA:abarla@cells.es,\ + "%MVALVIDARES:mvalvidares@cells.es,SMS:+34663695459",\ + "%IGOR:isics@cells.es,SMS:+34653579972",\ + "%AGNETA:svensson@cells.es,SMS:+34654605721",\ + "%KLEMENTIEV:kklementiev@cells.es,SMS:+34634852597",\ + "%LUCIA:laballe@cells.es,SMS:+34617564624",\ + "%ERIC:epellegrin@cells.es,SMS:+34637029542",\ + "%MVLUCIA:laballe@cells.es,SMS:+34617564624",\ + "%JUANHUIX:juanhuix@cells.es,SMS:+34655123696",\ + "%JBENACH:jbenach@cells.es,SMS:+34608107080",\ + "%SILVIA:sforcat@cells.es,SMS:+34659311242",\ + "%GEMMA:gguilera@cells.es,SMS:+34646662234",\ + "%MARIA:mbrzhezinskaya@cells.es ,SMS:+34600736091",\ + "%EVA:epereiro@cells.es,SMS:+34630115585",\ + "%VIRGINIA:vperez@cells.es,SMS:+34689452160",\ + "%INMA:iperal@cells.es,SMS:+34678021620",\ + "%MICHAEL:mknapp@cells.es,SMS:+34672065901",\ + "%NICO:josep.nicolas@cells.es,SMS:+34669012168",\ + %FRANCOIS:ffauth@cells.es,\ + "%MARION:mkuhlmann@cells.es,SMS:+34652583204",\ + "",\ + "# On Call",\ + "%OC_VC:jpasquaud@cells.es,SMS:+34651436676,jnavarro@cells.es,SMS:+34670247608",\ + "%OC_CT:srubio@cells.es,SMS:+34606330920",\ + "",\ + "# RF",\ + "%BEA:bbravo@cells.es,SMS:+34608595343",\ + "%ANGELA:asalom@cells.es,SMS:+34620076431",\ + "%JESUS:jocampo@cells.es,SMS:+34696037994",\ + "%CONTROLROOM:,SMS:+34646291497",\ + "%DCALDERON:659850116,SMS:659850116" +CLASS/PyAlarm->SMSConfig: controls@cells:cells.cells + + +# --- dserver/PyAlarm/test properties + +dserver/PyAlarm/test->__SubDevices: "dserver/snapextractor/1",\ + "dserver/snapmanager/1",\ + "dserver/snaparchiver/1",\ + "test/sim/test-00"