/* * This file is part of the OpenSCADA project * Copyright (C) 2006-2011 TH4 SYSTEMS GmbH (http://th4-systems.com) * * OpenSCADA is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 * only, as published by the Free Software Foundation. * * OpenSCADA is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License version 3 for more details * (a copy is included in the LICENSE file that accompanied this code). * * You should have received a copy of the GNU Lesser General Public License * version 3 along with OpenSCADA. If not, see * <http://opensource.org/licenses/lgpl-3.0.html> for a copy of the LGPLv3 License. */ package org.openscada.ae.ui.views.contributions; import java.io.IOException; import java.net.URL; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Observable; import java.util.Observer; import java.util.concurrent.atomic.AtomicInteger; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Clip; import javax.sound.sampled.DataLine; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; import org.eclipse.core.commands.ParameterizedCommand; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.resource.ResourceManager; import org.eclipse.swt.SWT; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.ui.PartInitException; import org.eclipse.ui.handlers.IHandlerService; import org.eclipse.ui.menus.WorkbenchWindowControlContribution; import org.openscada.ae.MonitorStatus; import org.openscada.ae.ui.views.Activator; import org.openscada.ae.ui.views.config.AlarmNotifierConfiguration; import org.openscada.ae.ui.views.config.ConfigurationHelper; import org.openscada.ae.ui.views.preferences.PreferenceConstants; import org.openscada.core.Variant; import org.openscada.core.client.ConnectionState; import org.openscada.core.connection.provider.ConnectionIdTracker; import org.openscada.core.connection.provider.ConnectionTracker; import org.openscada.core.connection.provider.ConnectionTracker.Listener; import org.openscada.da.client.DataItem; import org.openscada.da.client.DataItemValue; import org.openscada.da.connection.provider.ConnectionService; import org.openscada.ui.utils.blink.Blinker; import org.openscada.ui.utils.blink.Blinker.Handler; import org.openscada.ui.utils.blink.Blinker.State; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AlarmNotifier extends WorkbenchWindowControlContribution { private static final List<String> ALARM_STATES = Arrays.asList ( new String[] { "NOT_OK", "NOT_OK_AKN", "NOT_OK_NOT_AKN" } ); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ private static final List<String> ACK_STATES = Arrays.asList ( new String[] { "NOT_AKN", "NOT_OK_NOT_AKN" } ); //$NON-NLS-1$ //$NON-NLS-2$ private static final Logger logger = LoggerFactory.getLogger ( AlarmNotifier.class ); public static final String ID = "org.openscada.ae.ui.views.contributions.alarmnotifier"; //$NON-NLS-1$ private ResourceManager resourceManager; private Label label; private ParameterizedCommand ackAlarmsAvailableCommand; private ParameterizedCommand alarmsAvailableCommand; private String connectionId; protected ConnectionService connectionService; private ConnectionIdTracker connectionTracker; private String prefix; private final Map<String, AtomicInteger> monitorStatus = new HashMap<String, AtomicInteger> (); private Composite panel; private Clip clip; private URL soundFile; private volatile boolean connected = false; private final Collection<DataItem> items = new HashSet<DataItem> (); private Display display; private Label bellIcon; private Blinker blinker; public AlarmNotifier () { super (); } public AlarmNotifier ( final String id ) { super ( id ); } @Override public void dispose () { onDisconnect (); if ( this.blinker != null ) { this.blinker.dispose (); } this.resourceManager.dispose (); super.dispose (); } @Override protected Control createControl ( final Composite parent ) { this.display = parent.getDisplay (); this.resourceManager = new LocalResourceManager ( JFaceResources.getResources () ); initMonitorStates (); this.panel = new Composite ( parent, SWT.NONE ); final GridLayout layout = new GridLayout ( 2, false ); layout.marginHeight = layout.marginWidth = 0; this.panel.setLayout ( layout ); this.panel.setCursor ( this.display.getSystemCursor ( SWT.CURSOR_HAND ) ); // this.panel.addMouseListener ( this ); this.label = new Label ( this.panel, SWT.NONE ); this.label.setText ( getLabel () ); this.label.setAlignment ( SWT.CENTER ); GridData gd = new GridData ( SWT.CENTER, SWT.CENTER, true, true ); gd.widthHint = gd.minimumWidth = 110; this.label.setLayoutData ( gd ); this.label.addMouseListener ( new MouseAdapter () { @Override public void mouseUp ( final MouseEvent e ) { triggerMainCommand (); }; } ); this.bellIcon = new Label ( this.panel, SWT.NONE ); this.bellIcon.setAlignment ( SWT.CENTER ); gd = new GridData ( SWT.FILL, SWT.FILL, false, true ); gd.widthHint = gd.minimumWidth = getBellIcon ().getBounds ().width; this.bellIcon.setLayoutData ( gd ); this.bellIcon.addMouseListener ( new MouseAdapter () { @Override public void mouseUp ( final MouseEvent e ) { triggerBellSwitch (); } } ); this.blinker = new Blinker ( new Handler () { @Override public void setState ( final State state ) { AlarmNotifier.this.setBlinkerState ( state ); } } ); this.blinker.setState ( false, false, false, true, false, false ); loadConfiguration (); return this.panel; } private void initMonitorStates () { for ( final MonitorStatus ms : MonitorStatus.values () ) { this.monitorStatus.put ( ms.name (), new AtomicInteger ( 0 ) ); } } protected void setBlinkerState ( final State state ) { trigger ( new Runnable () { @Override public void run () { if ( AlarmNotifier.this.panel.isDisposed () ) { return; } switch ( state ) { case ERROR: case DISCONNECTED: setBackground ( AlarmNotifier.this.display.getSystemColor ( SWT.COLOR_MAGENTA ) ); break; case ALARM_1: case ALARM: setBackground ( AlarmNotifier.this.display.getSystemColor ( SWT.COLOR_RED ) ); break; case OK: case ALARM_0: default: setBackground ( null ); break; } } } ); } protected void setBackground ( final Color color ) { this.panel.setBackground ( color ); this.label.setBackground ( color ); this.bellIcon.setBackground ( color ); } protected void triggerBellSwitch () { try { this.connectionService.getConnection ().write ( getItemId ( "ALERT_ACTIVE" ), Variant.FALSE, null ); //$NON-NLS-1$ } catch ( final Exception e ) { Activator.getDefault ().getLog ().log ( new Status ( IStatus.ERROR, Activator.PLUGIN_ID, Messages.AlarmNotifier_Status_ErrorWriteBellCommand, e ) ); } } private void loadConfiguration () { final AlarmNotifierConfiguration cfg = ConfigurationHelper.findAlarmNotifierConfiguration (); if ( cfg != null ) { try { setConfiguration ( cfg ); } catch ( final Exception e ) { logger.warn ( "Failed to apply configuration", e ); //$NON-NLS-1$ } } else { logger.info ( "no configuration found" ); //$NON-NLS-1$ } } private void setConfiguration ( final AlarmNotifierConfiguration cfg ) throws UnsupportedAudioFileException, IOException, LineUnavailableException { this.connectionId = cfg.getConnectionId (); this.prefix = cfg.getPrefix (); this.soundFile = cfg.getSoundFile (); this.ackAlarmsAvailableCommand = cfg.getAckAlarmsAvailableCommand (); this.alarmsAvailableCommand = cfg.getAlarmsAvailableCommand (); initConnection (); } private void initConnection () { if ( this.connectionId == null ) { return; } final ConnectionTracker.Listener connectionServiceListener = new Listener () { @Override public void setConnection ( final org.openscada.core.connection.provider.ConnectionService connectionService ) { AlarmNotifier.this.setConnectionService ( connectionService ); } }; this.connectionTracker = new ConnectionIdTracker ( Activator.getDefault ().getBundle ().getBundleContext (), this.connectionId, connectionServiceListener ); this.connectionService = null; this.connectionTracker.open (); } private void onDisconnect () { for ( final DataItem item : this.items ) { item.deleteObservers (); item.unregister (); } this.items.clear (); initMonitorStates (); this.connected = false; disableHorn (); updateAlarms (); } private String getItemId ( final String localId ) { return this.prefix + "." + localId; //$NON-NLS-1$ } private void onConnect () { for ( final MonitorStatus ms : MonitorStatus.values () ) { final String id = getItemId ( ms.name () ); final DataItem item = new DataItem ( id ); item.addObserver ( new Observer () { @Override public void update ( final Observable o, final Object arg ) { updateMonitorStatus ( ms, (DataItemValue)arg ); } } ); item.register ( this.connectionService.getItemManager () ); this.items.add ( item ); } final String id = getItemId ( "ALERT_ACTIVE" ); //$NON-NLS-1$ final DataItem item = new DataItem ( id ); item.addObserver ( new Observer () { @Override public void update ( final Observable o, final Object arg ) { trigger ( new Runnable () { @Override public void run () { updateActiveState ( (DataItemValue)arg ); } } ); } } ); item.register ( this.connectionService.getItemManager () ); this.items.add ( item ); } private void triggerMainCommand () { try { if ( numberOfAckAlarms () > 0 ) { executeCommand ( this.ackAlarmsAvailableCommand ); } else { executeCommand ( this.alarmsAvailableCommand ); } } catch ( final PartInitException ex ) { throw new RuntimeException ( ex ); } } private void executeCommand ( final ParameterizedCommand command ) throws PartInitException { final IHandlerService handlerService = (IHandlerService)getWorkbenchWindow ().getService ( IHandlerService.class ); if ( command.getCommand ().isDefined () ) { try { handlerService.executeCommand ( command, null ); } catch ( final Exception e ) { throw new RuntimeException ( e ); } } } protected void trigger ( final Runnable run ) { if ( this.display == null || this.display.isDisposed () ) { return; } this.display.asyncExec ( new Runnable () { @Override public void run () { if ( AlarmNotifier.this.display.isDisposed () ) { return; } run.run (); } } ); } private void updateAlarms () { trigger ( new Runnable () { @Override public void run () { if ( !AlarmNotifier.this.panel.isDisposed () && !AlarmNotifier.this.label.isDisposed () ) { updateState (); AlarmNotifier.this.label.setText ( getLabel () ); } } } ); } protected void updateState () { this.blinker.setState ( numberOfAlarms () > 0, numberOfAckAlarms () > 0, false, !this.connected, false, false ); } private void disableHorn () { if ( this.clip != null ) { this.clip.stop (); this.clip.close (); this.clip = null; } if ( !this.bellIcon.isDisposed () ) { this.bellIcon.setImage ( null ); } } private void enableHorn () throws UnsupportedAudioFileException, IOException, LineUnavailableException { if ( ( this.clip == null || !this.clip.isRunning () ) && Activator.getDefault ().getPreferenceStore ().getBoolean ( PreferenceConstants.BELL_ACTIVATED_KEY ) ) { final AudioInputStream sound = AudioSystem.getAudioInputStream ( this.soundFile ); final DataLine.Info info = new DataLine.Info ( Clip.class, sound.getFormat () ); this.clip = (Clip)AudioSystem.getLine ( info ); this.clip.open ( sound ); this.clip.loop ( Clip.LOOP_CONTINUOUSLY ); } if ( !this.bellIcon.isDisposed () ) { this.bellIcon.setImage ( getBellIcon () ); } } private Image getBellIcon () { return this.resourceManager.createImageWithDefault ( ImageDescriptor.createFromFile ( AlarmNotifier.class, "icons/bell.png" ) ); //$NON-NLS-1$ } private int numberOfAckAlarms () { int alarms = 0; for ( final Entry<String, AtomicInteger> entry : this.monitorStatus.entrySet () ) { if ( ACK_STATES.contains ( entry.getKey () ) ) { alarms += entry.getValue ().get (); } } return alarms; } private int numberOfAlarms () { int alarms = 0; for ( final Entry<String, AtomicInteger> entry : this.monitorStatus.entrySet () ) { if ( ALARM_STATES.contains ( entry.getKey () ) ) { alarms += entry.getValue ().get (); } } return alarms; } private String getLabel () { if ( this.connectionService == null || this.connectionService.getConnection ().getState () != ConnectionState.BOUND ) { return Messages.AlarmNotifier_Label_State_Disconnected; } if ( numberOfAlarms () + numberOfAckAlarms () == 0 ) { return Messages.AlarmNotifier_Label_State_NoAlarm; } return String.format ( Messages.AlarmNotifier_Label_State_AlarmsFormat, numberOfAckAlarms (), numberOfAlarms () ); } private void setConnectionService ( final org.openscada.core.connection.provider.ConnectionService connectionService ) { if ( connectionService == null ) { onDisconnect (); AlarmNotifier.this.connectionService = null; return; } else { AlarmNotifier.this.connectionService = (ConnectionService)connectionService; onConnect (); } } protected void updateMonitorStatus ( final MonitorStatus ms, final DataItemValue value ) { this.monitorStatus.get ( ms.name () ).set ( value.getValue ().asInteger ( 0 ) ); updateAlarms (); } protected void updateActiveState ( final DataItemValue value ) { AlarmNotifier.this.connected = value.isConnected (); updateState (); if ( value.getValue ().asBoolean ( false ) ) { try { enableHorn (); } catch ( final Exception e ) { logger.error ( "could not play sound!", e ); //$NON-NLS-1$ } } else { disableHorn (); } } }