/** * * Copyright (c) 2009-2014 Freedomotic team http://freedomotic.com * * This file is part of Freedomotic * * This Program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2, or (at your option) any later version. * * This Program 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 General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * Freedomotic; see the file COPYING. If not, see * <http://www.gnu.org/licenses/>. */ package com.freedomotic.core; import com.freedomotic.api.AbstractConsumer; import com.freedomotic.api.Client; import com.freedomotic.api.EventTemplate; import com.freedomotic.app.Freedomotic; import com.freedomotic.bus.BusService; import com.freedomotic.exceptions.RepositoryException; import com.freedomotic.exceptions.UnableToExecuteException; import com.freedomotic.model.object.Behavior; import com.freedomotic.plugins.ClientStorage; import com.freedomotic.plugins.ObjectPluginPlaceholder; import com.freedomotic.things.ThingRepository; import com.freedomotic.reactions.Command; import com.freedomotic.reactions.CommandRepository; import com.freedomotic.reactions.TriggerRepository; import com.freedomotic.things.EnvObjectLogic; import com.google.inject.Inject; import java.io.File; import java.io.IOException; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Create Things on the fly using a template as a starting point. It can be * created from a Command or by calling the * {@link #join(String clazz, String name, String protocol, String address, boolean allowClones)} * method. This module spawns a new instance overriding template properties like * name, protocol and address. Moreover it can load the hardware triggers and * commands mapping from the tuples defined in the plugin manifest. * * @author Enrico Nicoletti */ public final class Autodiscovery extends AbstractConsumer { private static final String MESSAGING_CHANNEL = "app.objects.create"; private static final Logger LOG = LoggerFactory.getLogger(Autodiscovery.class.getName()); // Dependencies private final ClientStorage clientStorage; private final ThingRepository thingsRepository; private final TriggerRepository triggerRepository; private final CommandRepository commandRepository; @Inject Autodiscovery(ClientStorage clientStorage, ThingRepository thingsRepository, TriggerRepository triggerRepository, CommandRepository commandRepository, BusService busService) { super(busService); this.clientStorage = clientStorage; this.thingsRepository = thingsRepository; this.triggerRepository = triggerRepository; this.commandRepository = commandRepository; } /** * * @return */ @Override public String getMessagingChannel() { return MESSAGING_CHANNEL; } @Override protected void onCommand(Command command) throws IOException, UnableToExecuteException { String name = command.getProperty("object.name"); String protocol = command.getProperty("object.protocol"); String address = command.getProperty("object.address"); String clazz = command.getProperty("object.class"); // Creates a Thing also if one with the same name exists, true by default boolean allowClones = command.getBooleanProperty("autodiscovery.allow-clones", true); try { join(clazz, name, protocol, address, allowClones); } catch (RepositoryException ex) { LOG.error(Freedomotic.getStackTraceInfo(ex)); } } /** * Creates an {@link EnvObjectLogic} with the specification in input. If an * {@link EnvObjectLogic} with the same protocol and address already exists * it will exit with no changes. * * @param clazz the name of the thing template to load (eg: Light) * @param name the name of the thing * @param protocol the protocol which drives the thing * @param address the address to uniquely identify the thing * @param allowClones determines if an existent template can be cloned by * adding an ordinal number to its name, true by default * @return the thing created * @throws com.freedomotic.exceptions.RepositoryException if it's not * possible to retrieve the requested thing information */ protected EnvObjectLogic join(String clazz, String name, String protocol, String address, boolean allowClones) throws RepositoryException { // Check if autodiscovery can be applied if (thingAlreadyExists(protocol, address)) { LOG.info("A thing with protocol \"{}\" and address \"{}\" already exists in the environment. Autodiscovery exits without changes", new Object[]{protocol, address}); return null; } // If not allowed to clone an Thing if (!allowClones && thingAlreadyExists(name)) { LOG.info("A thing with name \"{}\" already exists in the environment. Autodiscovery exits without changes because property 'autodiscovery.allow-clones' property is ''{}'' for the received command", new Object[]{name, allowClones}); return null; } // Check if the requested Thing template is loaded ObjectPluginPlaceholder thingTemplate = (ObjectPluginPlaceholder) clientStorage.get(clazz); if (thingTemplate == null) { LOG.warn("Autodiscovery error: doesn't exist an object class called \"{}\"", clazz); return null; } // Start the new Thing creation LOG.warn("Autodiscovery request for an object called \"{}\" of type \"{}\"", new Object[]{name, clazz}); File templateFile = thingTemplate.getTemplate(); EnvObjectLogic loaded = thingsRepository.load(templateFile); //changing the name and other properties invalidates related trigger and commands //call init() again after this changes if ((name != null) && !name.isEmpty()) { loaded.getPojo().setName(name); } else { loaded.getPojo().setName(protocol); } loaded = thingsRepository.copy(loaded); loaded.getPojo().setProtocol(protocol); loaded.getPojo().setPhisicalAddress(address); // Remove the 'virtual' tag and any other actAs configuration. //TODO: it would be better to remove the actAs property and manage all with tags loaded.getPojo().setActAs(""); loaded.setRandomLocation(); configureOptionalMapping(protocol, clazz, loaded); LOG.info("Autodiscovery adds a thing called \"{}\" of type \"{}\"", new Object[]{loaded.getPojo().getName(), clazz}); return loaded; } /** * Sets the PREFERRED MAPPING of the protocol plugin, if any is defined in * its manifest. The mapping information will be asked to the plugin which * implements the same protocol as the one specified in the arguments of * this method. * * The clazz argument is used to identify a tuple in the plugin manifest. * The configuration specified in this tuple will be applied to the target * Thing. * * @param protocol the thing protocol, used to retrieve the corresponding * plugin * @param clazz the dot notation taxonomy used to identify the mapping that * should be applied (eg: EnvObject.ElectricDevice.Light) * @param loaded the target thing * @throws RuntimeException */ private void configureOptionalMapping(String protocol, String clazz, EnvObjectLogic loaded) throws RuntimeException { Client addon = clientStorage.getClientByProtocol(protocol); if ((addon != null) && (addon.getConfiguration().getTuples() != null)) { for (int i = 0; i < addon.getConfiguration().getTuples().size(); i++) { Map tuple = addon.getConfiguration().getTuples().getTuple(i); String regex = (String) tuple.get("object.class"); if ((regex != null) && clazz.matches(regex)) { //map object behaviors to hardware triggers for (Behavior behavior : loaded.getPojo().getBehaviors()) { String triggerName = (String) tuple.get(behavior.getName()); if (triggerName != null) { loaded.addTriggerMapping(triggerRepository.findByName(triggerName).get(0), behavior.getName()); } } for (String action : loaded.getPojo().getActions().stringPropertyNames()) { String commandName = (String) tuple.get(action); if (commandName != null) { List<Command> list = commandRepository.findByName(commandName); if (!list.isEmpty()) { loaded.setAction(action, list.get(0)); } else { throw new RuntimeException("No commands found with name " + commandName); } } } } } } } private boolean thingAlreadyExists(String protocol, String address) { if (protocol.trim().equalsIgnoreCase("unknown")) { return false; //Multiple object with protocol 'unknown' are allowed } return !(thingsRepository.findByAddress(protocol, address) == null); } private boolean thingAlreadyExists(String name) { return !thingsRepository.findByName(name).isEmpty(); } @Override protected void onEvent(EventTemplate event) { throw new UnsupportedOperationException("Autodiscovery module is not supposed to receive events"); } }