/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.anel.internal; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.openhab.binding.anel.AnelBindingProvider; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.library.types.OnOffType; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The actual Anel binding. Please see javadoc of * {@link AnelGenericBindingProvider} how to use it. * * @author paphko * @since 1.6.0 */ public class AnelBinding extends AbstractActiveBinding<AnelBindingProvider>implements ManagedService { /** * Delay before first initial refresh call for initialization (required at * any time after startup to make sure everything else is initialized). */ private static final int THREAD_INITIALIZATION_DELAY = 60000; /** Interruption timeout when disconnecting all threads. */ private static final int THREAD_INTERRUPTION_TIMEOUT = 5000; /** Logger for this binding class. */ private static final Logger logger = LoggerFactory.getLogger(AnelBinding.class); /** * Internally used for communication with connector thread. */ static interface IInternalAnelBinding { /** * Get all item names that are registered for the given command type. * * @param device * The device name for which items should be searched. * @param cmd * A command type. * @return All item names that are registered for the given command * type. */ Collection<String> getItemNamesForCommandType(String device, AnelCommandType cmd); /** * Connectors should use this to send updates to the event bus. * * @param itemName * The item name (from * {@link #getItemNamesForCommandType(AnelCommandType)}) * whose state was updated. * @param newState * The new state to be sent to the event bus. */ void postUpdateToEventBus(String itemName, State newState); } /** The refresh interval which is used to poll values from the Anel server. */ private long refreshInterval = AnelConfigReader.DEFAULT_REFRESH_INTERVAL; /** Threads to communicate with Anel devices */ private final Map<String, AnelConnectorThread> connectorThreads = new HashMap<String, AnelConnectorThread>(); /** Binding facade used by the {@link #connectorThreads}. */ private final IInternalAnelBinding bindingFacade = new IInternalAnelBinding() { @Override public Collection<String> getItemNamesForCommandType(String device, AnelCommandType cmd) { return AnelBinding.this.getItemNamesForCommandType(device, cmd); } @Override public void postUpdateToEventBus(String itemName, State newState) { eventPublisher.postUpdate(itemName, newState); } }; @Override public void activate() { // don't do anything because we are still waiting for the update-call to // get config } @Override public void deactivate() { // deallocate resources here that are no longer needed and // should be reset when activating this binding again disconnectAll(); connectorThreads.clear(); } /** * {@inheritDoc} */ @Override protected long getRefreshInterval() { return refreshInterval; } /** * {@inheritDoc} */ @Override protected String getName() { return "Anel NET-PwrCtrl Service"; } /** * {@inheritDoc} */ @Override protected void execute() { // the frequently executed code (polling) goes here ... /* * poll the device for its state regularly, just in case UDP packets * might be missed. Do that only when providers exist! */ if (refreshInterval > 0 && bindingsExist()) { refreshAll(); } } /** * Request a refresh on all threads. */ private void refreshAll() { for (AnelConnectorThread connectorThread : connectorThreads.values()) { connectorThread.requestRefresh(); } } /** * Disconnect all currently running listener threads. */ private void disconnectAll() { // first, notify all existing connector threads to interrupt for (String device : connectorThreads.keySet()) { logger.debug("Close message listener for device '" + device + "'"); final AnelConnectorThread connectorThread = connectorThreads.get(device); connectorThread.setInterrupted(); } // then wait for all of them to die for (String device : connectorThreads.keySet()) { final AnelConnectorThread connectorThread = connectorThreads.get(device); try { // wait for the thread to die with a timeout of 5 seconds connectorThread.join(THREAD_INTERRUPTION_TIMEOUT); } catch (InterruptedException e) { logger.info("Previous message listener closing interrupted for device '" + device + "'", e); } } } /** * There is no need to react on status changes because we only listen to * commands. */ @Override protected void internalReceiveUpdate(String itemName, State newState) { } /** * Valid commands are of type {@link OnOffType} for items bound to * {@link AnelCommandType#F1} - {@link AnelCommandType#F8} or * {@link AnelCommandType#IO1} - {@link AnelCommandType#IO8}. */ @Override protected void internalReceiveCommand(String itemName, Command command) { logger.trace("Received command (item='{}', command='{}')", itemName, command.toString()); final Map<String, AnelCommandType> map = getCommandTypeForItemName(itemName); if (map == null || map.isEmpty()) { logger.debug("Invalid command for item name: '" + itemName + "'"); return; } final String deviceId = map.keySet().iterator().next(); final AnelConnectorThread connectorThread = connectorThreads.get(deviceId); if (connectorThread == null) { logger.debug("Could not find device '" + deviceId + "', missing configuration or not yet initialized."); return; } final AnelCommandType cmd = map.get(deviceId); // check for switchable command final boolean isSwitch = AnelCommandType.SWITCHES.contains(cmd); final boolean isIO = AnelCommandType.IOS.contains(cmd); if (isIO || isSwitch) { if (command instanceof OnOffType) { final boolean newStateBoolean = OnOffType.ON.equals(command); if (isSwitch) { final int switchNr = Integer.parseInt(cmd.name().substring(1)); connectorThread.sendSwitch(switchNr, newStateBoolean); } else if (isIO) { final int ioNr = Integer.parseInt(cmd.name().substring(2)); connectorThread.sendIO(ioNr, newStateBoolean); } } else { logger.warn("Invalid state for '" + cmd.name() + "' (expected: ON/OFF): " + command); } } else { logger.warn("Cannot switch '" + cmd.name() + "', supported switches: F1 - F8, IO1 - IO8"); } } protected void addBindingProvider(AnelBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(AnelBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } /** * {@inheritDoc} */ @Override public void updated(Dictionary<String, ?> config) throws ConfigurationException { // disconnect all currently running threads disconnectAll(); // clear map of previous threads because config changed connectorThreads.clear(); refreshInterval = AnelConfigReader.DEFAULT_REFRESH_INTERVAL; // read new config try { refreshInterval = AnelConfigReader.readConfig(config, connectorThreads, bindingFacade); logger.debug("Anel configuration read with refresh interval " + refreshInterval + "ms and " + connectorThreads.size() + " devices."); } catch (ConfigurationException e) { logger.error("Could not read configuration for Anel binding: " + e.getMessage()); return; } catch (Exception e) { logger.error("Could not read configuration for Anel binding", e); return; } setProperlyConfigured(true); // successful! now start all device threads for (String device : connectorThreads.keySet()) { final AnelConnectorThread connectorThread = connectorThreads.get(device); logger.debug("Starting message listener for device: " + connectorThread); connectorThread.start(); } // start new thread that calls an initial refresh so the internal state // can be initialized new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(THREAD_INITIALIZATION_DELAY); } catch (InterruptedException e) { } refreshAll(); } }).start(); } private Collection<String> getItemNamesForCommandType(String device, AnelCommandType cmd) { if (cmd == null) { return Collections.emptyList(); } final Set<String> itemNames = new HashSet<String>(); for (final AnelBindingProvider provider : providers) { // for each provider, check all items for (final String itemName : provider.getItemNames()) { // we only want to collect items for our device if (device.equals(provider.getDeviceId(itemName))) { final AnelCommandType commandType = provider.getCommandType(itemName); // if the item type matches, item should be returned if (commandType.equals(cmd)) { itemNames.add(itemName); } } } } return itemNames; } private Map<String, AnelCommandType> getCommandTypeForItemName(String itemName) { if (itemName == null || itemName.isEmpty()) { return null; } for (final AnelBindingProvider provider : providers) { for (final String providerItemName : provider.getItemNames()) { if (itemName.equals(providerItemName)) { final AnelCommandType cmd = provider.getCommandType(itemName); final String deviceId = provider.getDeviceId(itemName); return Collections.singletonMap(deviceId, cmd); } } } return null; } }