/*
* AndFHEM - Open Source Android application to control a FHEM home automation
* server.
*
* Copyright (c) 2011, Matthias Klass or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU GENERAL PUBLIC LICENSE, as published by the Free Software Foundation.
*
* 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 this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package li.klass.fhem.service.intent.voice;
import android.content.Context;
import com.google.common.base.*;
import com.google.common.base.Optional;
import com.google.common.collect.*;
import li.klass.fhem.domain.LightSceneDevice;
import li.klass.fhem.domain.core.*;
import li.klass.fhem.service.room.RoomListService;
import javax.inject.*;
import java.util.*;
import static com.google.common.collect.FluentIterable.from;
@Singleton
public class VoiceCommandService {
private static final String COMMAND_START = "schal[kt]e|switch|set";
private static final String SET_COMMAND_START = "set";
private Map<String, String> START_REPLACE = ImmutableMap.<String, String>builder()
.put(COMMAND_START, "set").build();
private Map<String, String> STATE_REPLACE = ImmutableMap.<String, String>builder()
.put("an|[n]?ein|1", "on")
.put("aus", "off").build();
private static final Map<String, String> SHORTCUTS = ImmutableMap.<String, String>builder()
.put("starte", "on")
.put("beginne", "on")
.put("start", "on")
.put("begin", "on")
.put("end", "off")
.put("beende", "off")
.put("stoppe", "off")
.put("stop", "off")
.build();
private Set<String> FILL_WORDS_TO_REPLACE = Sets.newHashSet("der", "die", "das", "den", "the", "doch", "bitte", "please");
private RoomListService roomListService;
@Inject
public VoiceCommandService(RoomListService roomListService) {
this.roomListService = roomListService;
}
public Optional<VoiceResult> resultFor(String voiceCommand, Context context) {
voiceCommand = replaceArticles(voiceCommand.toLowerCase(Locale.getDefault()));
List<String> parts = Arrays.asList(voiceCommand.split(" "));
if (parts.isEmpty()) {
return Optional.absent();
}
Optional<VoiceResult> shortcutResult = handleShortcut(parts, context);
if (shortcutResult.isPresent()) {
return shortcutResult;
}
return handleSetCommand(parts, context);
}
private Optional<VoiceResult> handleShortcut(List<String> parts, Context context) {
Optional<String> shortcut = shortcutCommandFor(parts.get(0));
if (shortcut.isPresent() && parts.size() > 1) {
ImmutableList<String> partsToSet = ImmutableList.<String>builder()
.add(SET_COMMAND_START)
.addAll(parts.subList(1, parts.size()))
.add(shortcut.get())
.build();
return handleSetCommand(partsToSet, context);
}
return Optional.absent();
}
private Optional<String> shortcutCommandFor(String shortcut) {
return Optional.fromNullable(SHORTCUTS.get(shortcut));
}
private Optional<VoiceResult> handleSetCommand(List<String> parts, Context context) {
String starter = replace(parts.get(0), START_REPLACE);
if (!starter.equals(SET_COMMAND_START) || parts.size() < 3) return Optional.absent();
final String deviceName = Joiner.on(" ").join(parts.subList(1, parts.size() - 1));
final String state = replace(Iterables.getLast(parts), STATE_REPLACE);
RoomDeviceList devices = roomListService.getAllRoomsDeviceList(Optional.<String>absent(), context);
List<FhemDevice> deviceMatches = from(devices.getAllDevices()).filter(filterDevicePredicate(deviceName, state)).toList();
if (deviceMatches.isEmpty()) {
return Optional.<VoiceResult>of(new VoiceResult.Error(VoiceResult.ErrorType.NO_DEVICE_MATCHED));
} else if (deviceMatches.size() > 1) {
return Optional.<VoiceResult>of(new VoiceResult.Error(VoiceResult.ErrorType.MORE_THAN_ONE_DEVICE_MATCHES));
}
FhemDevice device = deviceMatches.get(0);
String targetState = device.getReverseEventMapStateFor(state);
if (device instanceof LightSceneDevice) {
targetState = "scene " + targetState;
}
return Optional.<VoiceResult>of(new VoiceResult.Success(device.getName(), targetState));
}
private String replaceArticles(String command) {
for (String article : FILL_WORDS_TO_REPLACE) {
command = command.replaceAll(" " + article + " ", " ");
}
return command;
}
private Predicate<FhemDevice> filterDevicePredicate(final String spokenDeviceName, final String state) {
return new Predicate<FhemDevice>() {
@Override
public boolean apply(FhemDevice device) {
assert device != null;
String spokenName = sanitizeName(spokenDeviceName);
String stateToLookFor = device.getReverseEventMapStateFor(state);
String alias = sanitizeName(device.getAlias());
String pronunciation = sanitizeName(device.getPronunciation());
String name = sanitizeName(device.getName());
return (spokenName.equalsIgnoreCase(alias)
|| spokenName.equalsIgnoreCase(name)
|| (spokenName.equalsIgnoreCase(pronunciation)))
&& (device.getSetList().contains(stateToLookFor)
|| (device instanceof LightSceneDevice && ((LightSceneDevice) device).getScenes().contains(state)));
}
};
}
private String sanitizeName(String name) {
return name == null
? ""
: name.replaceAll("[_\\.!? ]", "");
}
private String replace(String in, Map<String, String> toReplace) {
for (Map.Entry<String, String> entry : toReplace.entrySet()) {
in = in.replaceAll(entry.getKey(), entry.getValue());
}
return in;
}
}