/** * 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.jointspace.internal; import java.awt.Color; import java.io.IOException; import java.net.SocketTimeoutException; import java.util.Dictionary; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.openhab.binding.jointspace.JointSpaceBindingProvider; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; import org.openhab.core.types.Command; import org.openhab.io.net.actions.Ping; import org.openhab.io.net.http.HttpUtil; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Binding to handle communication with jointSPACE device. For items that are * configured for polling, the jointspace server is polled every * {@code refreshInterval} ms * * @author David Lenz * @since 1.5.0 */ public class JointSpaceBinding extends AbstractActiveBinding<JointSpaceBindingProvider>implements ManagedService { private static final Logger logger = LoggerFactory.getLogger(JointSpaceBinding.class); /** Constant which represents the content type <code>application/json</code> */ public final static String CONTENT_TYPE_JSON = "application/json"; public final static String PREFIX_HSB_TYPE = "HSB"; public final static String PREFIX_DECIMAL_TYPE = "DEC"; /** * the refresh interval which is used to poll values from the JointSpace * server (optional, defaults to 60000ms) */ private long refreshInterval = 60000; /** * The ip of the TV set */ private String ip; /** * The port of the TV set, (optional, defaults to 1925) */ private String port = "1925"; public JointSpaceBinding() { } @Override public void activate() { } @Override public void deactivate() { } /** * @{inheritDoc */ @Override protected long getRefreshInterval() { return refreshInterval; } /** * @{inheritDoc */ @Override protected String getName() { return "JointSpace Refresh Service"; } /** * @{inheritDoc * * Calls @see updateItemState() for all items with a "POLL" * command in the configuration */ @Override protected void execute() { logger.debug("Checking if host is available"); boolean success = false; int timeout = 5000; try { success = Ping.checkVitality(ip, 0, timeout); if (success) { logger.debug("established connection [host '{}' port '{}' timeout '{}']", new Object[] { ip, 0, timeout }); // After connection is possible, do the actual polling for (JointSpaceBindingProvider provider : providers) { for (String itemName : provider.getItemNames()) { String tvcommand = provider.getTVCommand(itemName, "POLL"); if (tvcommand != null) { updateItemState(itemName, tvcommand); } } } } else { logger.debug("couldn't establish network connection [host '{}' port '{}' timeout '{}']", new Object[] { ip, 0, timeout }); } } catch (SocketTimeoutException se) { logger.debug("timed out while connecting to host '{}' port '{}' timeout '{}'", new Object[] { ip, 0, timeout }); } catch (IOException ioe) { logger.debug("couldn't establish network connection [host '{}' port '{}' timeout '{}']", new Object[] { ip, 0, timeout }); } } /** * Parses an ambilight command and extracts the layers. for example * "ambilight[layer1[left]]" will return a list {"layer1","left"} * * @param command * ambilight command string. For example "ambilight[layer1].color * @return a stringlist containing all the layers present in the command */ private String[] command2LayerString(String command) { String[] temp = command.split("\\.")[0].split("\\["); String[] layer = null; if (temp.length > 1) { layer = new String[temp.length - 1]; System.arraycopy(temp, 1, layer, 0, temp.length - 1); layer[layer.length - 1] = layer[layer.length - 1].replace(']', ' ').trim(); } else { layer = null; } return layer; } /** * Polls the TV for the values specified in @see tvCommand and posts state * update for @see itemName Currently only the following commands are * available - "ambilight[...]" returning a HSBType state for the given * ambilight pixel specified in [...] - "volume" returning a DecimalType - * "volume.mute" returning 'On' or 'Off' - "source" returning a String with * selected source (e.g. "hdmi1", "tv", etc) * * @param itemName * @param tvCommand */ private void updateItemState(String itemName, String tvCommand) { if (tvCommand.contains("ambilight")) { String[] layer = command2LayerString(tvCommand); HSBType state = new HSBType(getAmbilightColor(ip + ":" + port, layer)); eventPublisher.postUpdate(itemName, state); } else if (tvCommand.contains("volume")) { if (tvCommand.contains("mute")) { eventPublisher.postUpdate(itemName, getTVVolume(ip + ":" + port).mute ? OnOffType.ON : OnOffType.OFF); } else { eventPublisher.postUpdate(itemName, new DecimalType(getTVVolume(ip + ":" + port).volume)); } } else if (tvCommand.contains("source")) { eventPublisher.postUpdate(itemName, new StringType(getSource(ip + ":" + port))); } else { logger.error("Could not parse item state\"" + tvCommand + "\" for polling"); return; } } /** * @{inheritDoc Processes the commands and maps them to jointspace commands */ @Override protected void internalReceiveCommand(String itemName, Command command) { if (itemName != null && !this.providers.isEmpty()) { JointSpaceBindingProvider provider = this.providers.iterator().next(); if (provider == null) { logger.warn("Doesn't find matching binding provider [itemName={}, command={}]", itemName, command); return; } logger.debug("Received command (item='{}', state='{}', class='{}')", new Object[] { itemName, command.toString(), command.getClass().toString() }); String tvCommandString = null; // first check if we can translate the command directly tvCommandString = provider.getTVCommand(itemName, command.toString()); // if not try some special notations if (tvCommandString == null) { if (command instanceof HSBType) { tvCommandString = provider.getTVCommand(itemName, "HSB"); } else if (command instanceof DecimalType) { tvCommandString = provider.getTVCommand(itemName, "DEC"); } if (tvCommandString == null) { tvCommandString = provider.getTVCommand(itemName, "*"); } } if (tvCommandString == null) { logger.warn("Unrecognized command \"{}\"", command.toString()); return; } if (tvCommandString.contains("key")) { logger.debug("Found a Key command: " + tvCommandString); String[] commandlist = tvCommandString.split("\\."); if (commandlist.length != 2) { logger.warn("wrong number of arguments for key command \"{}\". Should be key.X", tvCommandString); return; } String key = commandlist[1]; sendTVCommand(key, ip + ":" + port); } else if (tvCommandString.contains("ambilight")) { logger.debug("Found an ambilight command: {}", tvCommandString); String[] commandlist = tvCommandString.split("\\."); String[] layer = command2LayerString(tvCommandString); if (commandlist.length < 2) { logger.warn( "wrong number of arguments for ambilight command \"{}\". Should be at least ambilight.color, ambilight.mode.X, etc...", tvCommandString); return; } if (commandlist[1].contains("color")) { setAmbilightColor(ip + ":" + port, command, layer); } else if (commandlist[1].contains("mode")) { if (commandlist.length != 3) { logger.warn( "wrong number of arguments for ambilight.mode command \"{}\". Should be ambilight.mode.internal, ambilight.mode.manual, ambilight.mode.expert", tvCommandString); return; } setAmbilightMode(commandlist[2], ip + ":" + port); } } else if (tvCommandString.contains("volume")) { logger.debug("Found a Volume command: {}", tvCommandString); sendVolume(ip + ":" + port, command); } else if (tvCommandString.contains("source")) { logger.debug("Found a Source command: {}", tvCommandString); String[] commandlist = tvCommandString.split("\\."); if (commandlist.length < 2) { logger.warn("wrong number of arguments for source command \"{}\". Should be at least mode.X...", tvCommandString); return; } sendSource(ip + ":" + port, commandlist[1]); } else { logger.warn("Unrecognized tv command \"{}\". Only key.X or ambilight[].X is supported", tvCommandString); return; } } } /** * Gets the color for a specified ambilight pixel from the host and tries to * parse the returned json value * * @param host * hostname including port to query the jointspace api. * @param layers * a list of layers to the requested pixel. For example * [layer1[right[2]]] * @return Color of the ambilight pixel, or NULL if value could not be * retrieved */ private Color getAmbilightColor(String host, String[] layers) { logger.debug("Getting ambilight color for host {} for layers {}", host, layers); Color retval = new Color(0, 0, 0); String url = "http://" + host + "/1/ambilight/processed"; String ambilight_json = HttpUtil.executeUrl("GET", url, IOUtils.toInputStream(""), CONTENT_TYPE_JSON, 1000); if (ambilight_json != null) { logger.trace("TV returned for ambilight request: {}", ambilight_json); try { Object obj = JSONValue.parse(ambilight_json); JSONObject array = (JSONObject) obj; for (String layer : layers) { array = (JSONObject) array.get(layer.trim()); if (array == null) { logger.warn("Could not find layer {} in the json string", layer); return null; } } int r = 0, g = 0, b = 0; r = Integer.parseInt(array.get("r").toString()); g = Integer.parseInt(array.get("g").toString()); b = Integer.parseInt(array.get("b").toString()); retval = new Color(r, g, b); } catch (Throwable t) { logger.warn("Could not parse JSON String for ambilight value. Error: {}", t.toString()); } } else { logger.debug("Could not get ambilight value from JointSpace Server \"{}\"", host); return null; } return retval; } /** * Polls the source from the tv and returns it as a string * * @param host * @return a string containig the "source" returned by the TV, or null if * unsuccesfull */ private String getSource(String host) { String url = "http://" + host + "/1/sources/current"; String source_json = HttpUtil.executeUrl("GET", url, IOUtils.toInputStream(""), CONTENT_TYPE_JSON, 1000); logger.debug("Getting source for host {}", host); if (source_json != null) { logger.trace("TV returned for source request: {}", source_json); try { Object obj = JSONValue.parse(source_json); JSONObject array = (JSONObject) obj; return array.get("id").toString(); } catch (Throwable t) { logger.warn("Could not parse JSON String for source. Error: {}", t.toString()); } } else { logger.debug("Could not get source from JointSpace Server \"{}\"", host); } return null; } /** * Sets the current source at the TV * * @param host * @param source * string identifying the desired source. valid values are * "hdmi1", "tv", etc. (@see * http://jointspace.sourceforge.net/projectdata * /documentation/jasonApi/1/doc/API-Method-sources-GET.html) */ private void sendSource(String host, String source) { String url = "http://" + host + "/1/sources/current"; String content = "{\"id\":\"" + source + "\"}"; logger.debug("Switching source of host {} to {}", host, source); logger.trace(content.toString()); HttpUtil.executeUrl("POST", url, IOUtils.toInputStream(content), CONTENT_TYPE_JSON, 1000); } /** * Sends a volume command to the TV, depending on the command received For * commands of type DecimalType, the value for volume will be set directly * (mute will not be affected) For commands of type IncreaseDecreaseType, * the current value (polled from TV( for volume will be * incremented/decremented * * @param host * @param command */ private void sendVolume(String host, Command command) { logger.debug("Sending volume to host {} for command {}", host, command.toString()); volumeConfig conf = getTVVolume(host); String url = "http://" + host + "/1/audio/volume"; int newvalue = conf.volume; if (command instanceof DecimalType) { logger.debug("Setting volume to decimal type"); newvalue = ((DecimalType) command).intValue(); } else if (command instanceof IncreaseDecreaseType) { if ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE) { logger.debug("Increased volume"); newvalue++; } else { logger.debug("Decreased volume"); newvalue--; } } else { logger.warn( "Unitl now only DecimalType and IncreaseDecreaseType commands are supported vor volume command"); return; } // ensure that we are in the valid range for this device newvalue = Math.min(newvalue, conf.max); newvalue = Math.max(newvalue, conf.min); String content = "{\"muted\":\"" + conf.mute + "\", \"current\":\"" + newvalue + "\"}"; logger.trace(content); HttpUtil.executeUrl("POST", url, IOUtils.toInputStream(content), CONTENT_TYPE_JSON, 1000); } /** * Sets the ambilight color specified in command (which must be an HSBType * until now) for the pixel(s) specified with @see layers. * * @param host * @param command * HSBType command to set the color * @param layers * pixel(s) to set the color for. null if all pixels should have * the same value */ private void setAmbilightColor(String host, Command command, String[] layers) { if (!(command instanceof HSBType)) { logger.warn("Until now only HSBType is allowed for ambilight commands"); return; } logger.debug("Setting Ambilight color for host {} and layer {} to {}", host, layers, command.toString()); HSBType hsbcommand = (HSBType) command; String url = "http://" + host + "/1/ambilight/cached"; StringBuilder content = new StringBuilder(); content.append("{"); int count = 0; if (layers != null) { for (int i = 0; i < layers.length; i++) { content.append("\"" + layers[i] + "\":{"); count++; } } int red = Math.round(hsbcommand.getRed().floatValue() * 2.55f); int green = Math.round(hsbcommand.getGreen().floatValue() * 2.55f); int blue = Math.round(hsbcommand.getBlue().floatValue() * 2.55f); content.append("\"r\":" + red + ", \"g\":" + green + ", \"b\":" + blue); for (int i = 0; i < count; i++) { content.append("}"); } content.append("}"); logger.trace("Trying to post json for ambilight: {}", content.toString()); HttpUtil.executeUrl("POST", url, IOUtils.toInputStream(content.toString()), CONTENT_TYPE_JSON, 1000); } /** * Sends a key to to the host. Possible values for keys can be found here: * http * ://jointspace.sourceforge.net/projectdata/documentation/jasonApi/1/doc * /API-Method-input-key-POST.html * * @param key * @param host */ private void sendTVCommand(String key, String host) { logger.debug("Sending Key {} to {}", key, host); String url = "http://" + host + "/1/input/key"; String content = "{\"key\":\"" + key + "\"}"; logger.trace(content); HttpUtil.executeUrl("POST", url, IOUtils.toInputStream(content), CONTENT_TYPE_JSON, 1000); } /** * Function to query the TV Volume * * @param host * @return struct containing all given information about current volume * settings (volume, mute, min, max) @see volumeConfig */ private volumeConfig getTVVolume(String host) { volumeConfig conf = new volumeConfig(); String url = "http://" + host + "/1/audio/volume"; String volume_json = HttpUtil.executeUrl("GET", url, IOUtils.toInputStream(""), CONTENT_TYPE_JSON, 1000); if (volume_json != null) { try { Object obj = JSONValue.parse(volume_json); JSONObject array = (JSONObject) obj; conf.mute = Boolean.parseBoolean(array.get("muted").toString()); conf.volume = Integer.parseInt(array.get("current").toString()); conf.min = Integer.parseInt(array.get("min").toString()); conf.max = Integer.parseInt(array.get("max").toString()); } catch (NumberFormatException ex) { logger.warn("Exception while interpreting volume json return"); } catch (Throwable t) { logger.warn("Could not parse JSON String for volume value. Error: {}", t.toString()); } } return conf; } /** * Sets the mode of the ambilight processing mode. Manipulation the pixel * values cannot be done in "internal" mode * * For more details see: * http://jointspace.sourceforge.net/projectdata/documentation * /jasonApi/1/doc/API-Method-ambilight-mode-POST.html * * @param mode * possible modes are: "internal", "manual", "expert". * @param host */ private void setAmbilightMode(String mode, String host) { String url = "http://" + host + "/1/ambilight/mode"; String content = "{\"current\":\"" + mode + "\"}"; logger.trace(content); HttpUtil.executeUrl("POST", url, IOUtils.toInputStream(content), CONTENT_TYPE_JSON, 1000); } protected void addBindingProvider(JointSpaceBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(JointSpaceBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } /** * {@inheritDoc} */ @Override public void updated(Dictionary<String, ?> config) throws ConfigurationException { if (config != null) { // to override the default refresh interval one has to add a // parameter to openhab.cfg like // <bindingName>:refresh=<intervalInMs> String refreshIntervalString = (String) config.get("refreshinterval"); if (StringUtils.isNotBlank(refreshIntervalString)) { refreshInterval = Long.parseLong(refreshIntervalString); } String ipString = (String) config.get("ip"); if (StringUtils.isNotBlank(ipString)) { ip = ipString; setProperlyConfigured(true); } String portString = (String) config.get("port"); if (StringUtils.isNotBlank(portString)) { port = portString; } } } }