/** * 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.pilight.internal; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.openhab.binding.pilight.PilightBindingProvider; import org.openhab.binding.pilight.internal.communication.Action; import org.openhab.binding.pilight.internal.communication.Code; import org.openhab.binding.pilight.internal.communication.Config; import org.openhab.binding.pilight.internal.communication.Device; import org.openhab.binding.pilight.internal.communication.DeviceType; import org.openhab.binding.pilight.internal.communication.Status; import org.openhab.binding.pilight.internal.communication.Values; import org.openhab.binding.pilight.internal.types.PilightContactType; import org.openhab.core.binding.AbstractBinding; import org.openhab.core.binding.BindingProvider; import org.openhab.core.library.items.ContactItem; import org.openhab.core.library.items.DimmerItem; import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.items.StringItem; import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.StringType; 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; /** * Binding that communicates with one or multiple pilight instances. * * {@link http://www.pilight.org/} * * @author Jeroen Idserda * @since 1.0 */ public class PilightBinding extends AbstractBinding<PilightBindingProvider>implements ManagedService { private static final Logger logger = LoggerFactory.getLogger(PilightBinding.class); private static final int MAX_DIM_LEVEL = 15; private Map<String, PilightConnection> connections = new HashMap<String, PilightConnection>(); @Override public void deactivate() { logger.debug("pilight binding deactivated"); closeConnections(); } /** * Processes a status update received from pilight and changes the state of the * corresponding openHAB item (if item config is found) * * @param connection pilight connection * @param status The new Status */ private void processStatus(PilightConnection connection, Status status) { Integer type = status.getType(); if (!type.equals(DeviceType.SERVER)) { String instance = connection.getInstance(); String device = status.getDevices().get(0); List<PilightBindingConfig> configs = getConfigs(instance, device); if (!configs.isEmpty()) { if (type.equals(DeviceType.SWITCH) || type.equals(DeviceType.DIMMER)) { processSwitchEvent(configs, status); } else if (type.equals(DeviceType.CONTACT)) { processContactEvent(configs, status); } else if (type.equals(DeviceType.VALUE)) { processValueEvent(configs, status); } } } } private void processValueEvent(List<PilightBindingConfig> configs, Status status) { for (PilightBindingConfig config : configs) { String property = config.getProperty(); if (status.getValues().containsKey(property)) { String value = status.getValues().get(property); State state = getState(value, config); if (state != null) { eventPublisher.postUpdate(config.getItemName(), state); } } } } protected State getState(String value, PilightBindingConfig config) { State state = null; if (config.getItemType().equals(StringItem.class)) { state = new StringType(value); } else if (config.getItemType().equals(NumberItem.class)) { state = new DecimalType(new BigDecimal(value)); } return state; } private void processSwitchEvent(List<PilightBindingConfig> configs, Status status) { Integer type = status.getType(); State state = OnOffType.valueOf(status.getValues().get("state").toUpperCase()); if (type.equals(DeviceType.SWITCH)) { // noop, just use on/off state defined above } else if (type.equals(DeviceType.DIMMER)) { BigDecimal dimLevel = BigDecimal.ZERO; if (status.getValues().get("dimlevel") != null) { dimLevel = getPercentageFromDimLevel(status.getValues().get("dimlevel")); } else { // Dimmer items can can also be switched on or off in pilight. // When this happens, the dimmer value is not reported. At least we know it's on or off. dimLevel = state.equals(OnOffType.ON) ? new BigDecimal("100") : BigDecimal.ZERO; } state = new PercentType(dimLevel); } for (PilightBindingConfig config : configs) { eventPublisher.postUpdate(config.getItemName(), state); } } private void processContactEvent(List<PilightBindingConfig> configs, Status status) { String stateString = status.getValues().get("state").toUpperCase(); State state = PilightContactType.valueOf(stateString).toOpenClosedType(); for (PilightBindingConfig config : configs) { eventPublisher.postUpdate(config.getItemName(), state); } } /** * @{inheritDoc} */ @Override protected void internalReceiveCommand(String itemName, Command command) { PilightBindingProvider provider = findFirstMatchingBindingProvider(itemName); PilightBindingConfig config = provider.getBindingConfig(itemName); PilightConnection connection = connections.get(config.getInstance()); Action action = createUpdateCommand(command, config); if (action != null) { sendUpdate(action, connection); } } private void sendUpdate(Action action, PilightConnection connection) { connection.getConnector().doUpdate(action); } private Action createUpdateCommand(Command command, PilightBindingConfig config) { Action action = new Action(Action.ACTION_CONTROL); Code code = new Code(); code.setDevice(config.getDevice()); if (command instanceof OnOffType) { setOnOffValue((OnOffType) command, code); } else if (command instanceof PercentType) { setDimmerValue((PercentType) command, code); } else { logger.error("Only OnOffType and PercentType can be changed by the pilight binding"); return null; } action.setCode(code); return action; } private void setOnOffValue(OnOffType onOff, Code code) { code.setState(onOff.equals(OnOffType.ON) ? Code.STATE_ON : Code.STATE_OFF); } private void setDimmerValue(PercentType percent, Code code) { if (BigDecimal.ZERO.equals(percent.toBigDecimal())) { // pilight is not responding to commands that set both the dimlevel to 0 and state to off. // So, we're only updating the state for now code.setState(Code.STATE_OFF); } else { BigDecimal dimlevel = new BigDecimal(percent.toBigDecimal().toString()).setScale(2) .divide(BigDecimal.valueOf(100), RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(MAX_DIM_LEVEL)) .setScale(0, RoundingMode.HALF_UP); Values values = new Values(); values.setDimlevel(dimlevel.intValue()); code.setValues(values); code.setState(dimlevel.compareTo(BigDecimal.ZERO) == 1 ? Code.STATE_ON : Code.STATE_OFF); } } private PilightBindingProvider findFirstMatchingBindingProvider(String itemName) { for (PilightBindingProvider provider : providers) { if (provider.getItemNames().contains(itemName)) { return provider; } } return null; } private List<PilightBindingConfig> getConfigs(String instance, String device) { for (PilightBindingProvider provider : providers) { List<PilightBindingConfig> configs = provider.getBindingConfigs(instance, device); if (!configs.isEmpty()) { return configs; } } return new ArrayList<PilightBindingConfig>(); } protected void addBindingProvider(PilightBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(PilightBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } /** * {@inheritDoc} */ @Override public void updated(Dictionary<String, ?> config) throws ConfigurationException { closeConnections(); if (config != null) { this.connections = parseConfig(config); if (this.connections.size() > 0) { for (Entry<String, PilightConnection> entry : this.connections.entrySet()) { PilightConnection connection = entry.getValue(); startConnector(connection); } } } } private void closeConnections() { for (Entry<String, PilightConnection> entry : connections.entrySet()) { PilightConnection connection = entry.getValue(); if (connection.getConnector() != null) { connection.getConnector().close(); } } } private Map<String, PilightConnection> parseConfig(Dictionary<String, ?> config) { Map<String, PilightConnection> connections = new HashMap<String, PilightConnection>(); Enumeration<String> keys = config.keys(); while (keys.hasMoreElements()) { String key = keys.nextElement(); if ("service.pid".equals(key)) { continue; } String[] parts = key.split("\\."); String instance = parts[0]; PilightConnection connection = connections.get(instance); if (connection == null) { connection = new PilightConnection(); connection.setInstance(instance); } String value = ((String) config.get(key)).trim(); if ("host".equals(parts[1])) { connection.setHostname(value); } if ("port".equals(parts[1])) { connection.setPort(Integer.valueOf(value)); } if ("delay".equals(parts[1])) { connection.setDelay(Long.valueOf(value)); } connections.put(instance, connection); } return connections; } private void startConnector(PilightConnection connection) { connection.setConnector(new PilightConnector(connection, new IPilightMessageReceivedCallback() { @Override public void messageReceived(PilightConnection connection, Status status) { processStatus(connection, status); } })); connection.getConnector().start(); setInitialState(); } /** * Gets the state for item in {@code bindingConfig} from {@code pilightConfig} * * @param pilightConfig The complete pilight configuration * @param bindingConfig Specific pilight item in openHAB * @return Current state of the item */ private State getStateFromConfig(Config pilightConfig, PilightBindingConfig bindingConfig) { Device dev = pilightConfig.getDevices().get(bindingConfig.getDevice()); if (dev != null) { if (bindingConfig.getItemType().equals(SwitchItem.class) || bindingConfig.getItemType().equals(DimmerItem.class)) { OnOffType state = OnOffType.valueOf(dev.getState().toUpperCase()); if (dev.getDimlevel() != null && dev.getDimlevel() > 0) { if (state.equals(OnOffType.ON)) { return new PercentType(getPercentageFromDimLevel(dev.getDimlevel().toString())); } else { return new PercentType(0); } } return state; } else if (bindingConfig.getItemType().equals(ContactItem.class)) { OpenClosedType state = PilightContactType.valueOf(dev.getState().toUpperCase()).toOpenClosedType(); return state; } else if (bindingConfig.getItemType().equals(StringItem.class) || bindingConfig.getItemType().equals(NumberItem.class)) { String property = bindingConfig.getProperty(); if (dev.getProperties().containsKey(property)) { String value = dev.getProperties().get(property); return getState(value, bindingConfig); } } } return null; } /** * @{inheritDoc} */ @Override public void bindingChanged(BindingProvider provider, String itemName) { logger.debug("Binding changed for item {}", itemName); checkItemState(provider, itemName); } /** * @{inheritDoc} */ @Override public void allBindingsChanged(BindingProvider provider) { logger.debug("All bindings changed"); for (String itemName : provider.getItemNames()) { checkItemState(provider, itemName); } } private void setInitialState() { for (PilightBindingProvider provider : providers) { allBindingsChanged(provider); } } /** * Synchronize itemName with the current state in pilight. * * @param provider The PilightBindingProvider * @param itemName The itemName in openHAB */ private void checkItemState(BindingProvider provider, final String itemName) { PilightBindingProvider pilightProvider = (PilightBindingProvider) provider; final PilightBindingConfig config = pilightProvider.getBindingConfig(itemName); if (config != null) { PilightConnection connection = connections.get(config.getInstance()); if (connection.isConnected()) { connection.getConnector().refreshConfig(new IPilightConfigReceivedCallback() { @Override public void configReceived(PilightConnection connection) { State state = getStateFromConfig(connection.getConfig(), config); if (state != null) { eventPublisher.postUpdate(itemName, state); } } }); } } } private BigDecimal getPercentageFromDimLevel(String string) { return new BigDecimal(string).setScale(2).divide(new BigDecimal(MAX_DIM_LEVEL), RoundingMode.HALF_UP) .multiply(new BigDecimal(100)); } }