/** * 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.plex.internal; import static org.apache.commons.lang.StringUtils.*; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Objects; import org.openhab.binding.plex.PlexBindingProvider; import org.openhab.binding.plex.internal.annotations.ItemMapping; import org.openhab.binding.plex.internal.annotations.ItemPlayerStateMapping; import org.openhab.binding.plex.internal.communication.MediaContainer; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.binding.BindingProvider; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.OnOffType; 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.openhab.core.types.UnDefType; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Binding that communicates with a Plex Media Server * * {@link http://www.plex.tv/} * * @author Jeroen Idserda * @since 1.7.0 */ public class PlexBinding extends AbstractActiveBinding<PlexBindingProvider> { private static final Logger logger = LoggerFactory.getLogger(PlexBinding.class); private PlexConnector connector; private long refreshInterval = 5000; @Override protected void execute() { if (connector != null) { connector.refresh(); } } @Override protected long getRefreshInterval() { return refreshInterval; } @Override protected String getName() { return "Plex Refresh Service"; } protected void addBindingProvider(PlexBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(PlexBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } /** * @{inheritDoc} */ @Override public void bindingChanged(BindingProvider provider, String itemName) { logger.trace("Plex binding changed"); if (provider instanceof PlexBindingProvider) { PlexBindingProvider plexProvider = (PlexBindingProvider) provider; PlexBindingConfig config = plexProvider.getConfig(itemName); if (config != null) { setInitialState(config); } } } /** * {@inheritDoc} */ @Override public void allBindingsChanged(BindingProvider provider) { logger.trace("Plex all bindings changed"); if (provider instanceof PlexBindingProvider) { PlexBindingProvider plexProvider = (PlexBindingProvider) provider; setInitialState(plexProvider); } } /** * Set initial state for all items for all providers */ private void setInitialState() { for (PlexBindingProvider provider : providers) { setInitialState(provider); } } private void setInitialState(PlexBindingProvider provider) { for (String itemName : provider.getItemNames()) { PlexBindingConfig config = provider.getConfig(itemName); if (config != null) { setInitialState(config); } } } private void setInitialState(PlexBindingConfig config) { if (connector != null) { PlexSession session = connector.getSessionByMachineId(config.getMachineIdentifier()); if (session == null) { session = new PlexSession(); } updateConfigFromSession(config, session); } } /** * @{inheritDoc} */ @Override protected void internalReceiveCommand(String itemName, Command command) { PlexBindingConfig config = getConfig(itemName); if (!config.isReadOnly()) { try { connector.sendCommand(config, command); } catch (IOException e) { logger.error("Cannot send command {} for item {}", command, itemName, e); } } else { logger.warn("Cannot send command for item {}, property {} is read only", config.getItemName(), config.getProperty()); } } public void activate(final BundleContext bundleContext, final Map<String, Object> configuration) { configureBinding(configuration); } public void modified(final Map<String, Object> configuration) { disconnect(); configureBinding(configuration); } private void configureBinding(final Map<String, Object> configuration) { PlexConnectionProperties connectionProperties = new PlexConnectionProperties(); connectionProperties.setHost(Objects.toString(configuration.get("host"), null)); connectionProperties.setToken(Objects.toString(configuration.get("token"), null)); connectionProperties.setUsername(Objects.toString(configuration.get("username"), null)); connectionProperties.setPassword(Objects.toString(configuration.get("password"), null)); String port = Objects.toString(configuration.get("port"), null); if (isNotBlank(port) && isNumeric(port)) { connectionProperties.setPort(Integer.valueOf(port)); } String refresh = Objects.toString(configuration.get("refresh"), null); if (isNotBlank(refresh) && isNumeric(refresh)) { refreshInterval = Long.parseLong(refresh); } logger.debug("Plex config, server at {}:{}", connectionProperties.getHost(), connectionProperties.getPort()); if (isNotBlank(connectionProperties.getHost())) { try { connect(connectionProperties); setProperlyConfigured(true); } catch (UnknownHostException e) { setProperlyConfigured(false); logger.error("Cannot resolve Plex Media Server host: " + connectionProperties.getHost()); } } else { logger.warn("No host configured for Plex binding"); setProperlyConfigured(false); } } public void deactivate(final int reason) { logger.trace("Plex binding deactivated"); disconnect(); } private void disconnect() { if (connector != null) { connector.close(); } } /** * Get config from binding provider by Plex machine ID and property */ public PlexBindingConfig getConfig(String machineIdentifier, String property) { for (PlexBindingProvider provider : providers) { PlexBindingConfig config = provider.getConfig(machineIdentifier, property); if (config != null) { return config; } } return null; } /** * Get config from binding provider by itemName */ public PlexBindingConfig getConfig(String itemName) { for (PlexBindingProvider provider : providers) { PlexBindingConfig config = provider.getConfig(itemName); if (config != null) { return config; } } return null; } /** * Connect to the Plex server. * * @throws UnknownHostException If hostname is not resolvable. */ private void connect(PlexConnectionProperties connectionProperties) throws UnknownHostException { connector = new PlexConnector(connectionProperties, new PlexUpdateReceivedCallback() { @Override public void updateReceived(PlexSession session) { processUpdateRecevied(session); } @Override public void serverListUpdated(MediaContainer container) { processServerList(container); } }); connector.start(); setInitialState(); } /** * Player state update received from Plex. Update all items * for the machine ID this session is bound to. * * @param session Plex session */ private void processUpdateRecevied(PlexSession session) { for (PlexBindingProvider provider : providers) { for (String itemName : provider.getItemNames()) { PlexBindingConfig config = provider.getConfig(itemName); // In newer PMS versions, the machine identifier in the session also contains the type // of media that is playing (<id>_Video for example). We'll keep it backwards compatible // by only matching the first part of the machine identifier. if (session.getMachineIdentifier().startsWith(config.getMachineIdentifier())) { updateConfigFromSession(config, session); } } } } /** * Update all {@code PlexProperty.POWER} properties according to the * list of clients that are currently online. * * @param container MediaContainer, containing the clients that are currently online */ private void processServerList(MediaContainer container) { for (PlexBindingConfig config : getPowerConfigs()) { boolean online = container.getServer(config.getMachineIdentifier()) != null; eventPublisher.postUpdate(config.getItemName(), online ? OnOffType.ON : OnOffType.OFF); } } private List<PlexBindingConfig> getPowerConfigs() { List<PlexBindingConfig> configs = new ArrayList<PlexBindingConfig>(); for (PlexBindingProvider provider : providers) { Collection<String> itemNames = provider.getItemNames(); for (String itemName : itemNames) { PlexBindingConfig config = getConfig(itemName); if (config.getProperty().equals(PlexProperty.POWER.getName())) { configs.add(config); } } } return configs; } /** * Maps properties from {@code session} to openHAB item {@code config}. Mapping is specified by {@link ItemMapping} * annotations. * * @param config Binding config * @param session Plex session */ private void updateConfigFromSession(PlexBindingConfig config, PlexSession session) { String property = config.getProperty(); String itemName = config.getItemName(); PlexPlayerState state = session.getState(); for (Field field : session.getClass().getDeclaredFields()) { ItemMapping itemMapping = field.getAnnotation(ItemMapping.class); if (itemMapping != null) { if (itemMapping.property().getName().equals(property)) { if (itemMapping.type().equals(StringType.class)) { eventPublisher.postUpdate(itemName, getStringType(field, session)); } else if (itemMapping.type().equals(PercentType.class)) { eventPublisher.postUpdate(itemName, getPercenteType(field, session)); } else if (itemMapping.type().equals(DateTimeType.class)) { eventPublisher.postUpdate(itemName, getDateTimeType(field, session)); } } for (ItemPlayerStateMapping stateMapping : itemMapping.stateMappings()) { if (stateMapping.property().getName().equals(property)) { eventPublisher.postUpdate(itemName, state.equals(stateMapping.state()) ? OnOffType.ON : OnOffType.OFF); } } } } } private State getStringType(Field field, PlexSession session) { return new StringType(getStringProperty(field, session)); } private State getPercenteType(Field field, PlexSession session) { return new PercentType(getStringProperty(field, session)); } private State getDateTimeType(Field field, PlexSession session) { Date date = getDateProperty(field, session); if (date != null) { return new DateTimeType(getCalendar(date)); } else { return UnDefType.UNDEF; } } private Date getDateProperty(Field field, Object object) { Object value = invokeGetter(field, object); return value != null ? (Date) value : null; } private String getStringProperty(Field field, Object object) { Object value = invokeGetter(field, object); return value != null ? value.toString() : ""; } private Object invokeGetter(Field field, Object object) { try { return object.getClass().getMethod("get" + capitalize(field.getName())).invoke(object); } catch (IllegalAccessException e) { logger.debug("Error getting property value", e); } catch (NoSuchMethodException e) { logger.debug("Error getting property value", e); } catch (SecurityException e) { logger.debug("Error getting property value", e); } catch (InvocationTargetException e) { logger.debug("Error getting property value", e); } return null; } private Calendar getCalendar(Date date) { Calendar calendar = Calendar.getInstance(); calendar.setTime(date); return calendar; } }