├── TODO ├── src ├── test │ ├── java │ │ └── net │ │ │ └── whistlingfish │ │ │ └── harmony │ │ │ └── .keep │ └── resources │ │ ├── net │ │ └── whistlingfish │ │ │ └── harmony │ │ │ └── .keep │ │ └── logback.xml └── main │ ├── java │ └── net │ │ └── whistlingfish │ │ └── harmony │ │ ├── HarmonyHubListener.java │ │ ├── protocol │ │ ├── HarmonyProtocolException.java │ │ ├── AuthDetails.java │ │ ├── LoginToken.java │ │ ├── AuthFailedException.java │ │ ├── IrCommand.java │ │ ├── MessageGetConfig.java │ │ ├── MessagePing.java │ │ ├── EmptyIncrementedIdReplyFilter.java │ │ ├── HarmonyBindIQProvider.java │ │ ├── MessageGetCurrentActivity.java │ │ ├── MessageStartActivity.java │ │ ├── HarmonyXMPPTCPConnection.java │ │ ├── OAStanza.java │ │ ├── OAReplyFilter.java │ │ ├── MessageHoldAction.java │ │ ├── OAReplyProvider.java │ │ ├── OAReplyParser.java │ │ ├── MessageAuth.java │ │ └── EventStanza.java │ │ ├── shell │ │ ├── GetConfigCommand.java │ │ ├── ShowActivityCommand.java │ │ ├── ListDevicesCommand.java │ │ ├── ListActivitiesCommand.java │ │ ├── ShellCommand.java │ │ ├── StartActivityCommand.java │ │ ├── ShowCommand.java │ │ ├── PressButtonCommand.java │ │ ├── ListCommand.java │ │ └── ShellCommandWrapper.java │ │ ├── Jackson.java │ │ ├── ActivityChangeListener.java │ │ ├── config │ │ ├── Global.java │ │ ├── ControlGroup.java │ │ ├── Function.java │ │ ├── PowerState.java │ │ ├── Fixit.java │ │ ├── HarmonyConfig.java │ │ ├── Activity.java │ │ └── Device.java │ │ ├── ActivityStatusListener.java │ │ ├── HarmonyClientModule.java │ │ ├── Main.java │ │ └── HarmonyClient.java │ └── resources │ └── net │ └── whistlingfish │ └── harmony │ └── smack-providers.xml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml ├── LICENSE.EPL.md └── LICENSE.GPL.md /TODO: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/java/net/whistlingfish/harmony/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/net/whistlingfish/harmony/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /build 3 | .classpath 4 | .project 5 | .settings 6 | .idea 7 | .gradle 8 | /target/ 9 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/HarmonyHubListener.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony; 2 | 3 | /** 4 | * Marker interface for Harmony Hub notifications 5 | */ 6 | public interface HarmonyHubListener { 7 | void addTo(HarmonyClient harmonyClient); 8 | void removeFrom(HarmonyClient harmonyClient); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/net/whistlingfish/harmony/smack-providers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | oa 5 | connect.logitech.com 6 | net.whistlingfish.harmony.protocol.OAReplyProvider 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/protocol/HarmonyProtocolException.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.protocol; 2 | 3 | public class HarmonyProtocolException extends RuntimeException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public HarmonyProtocolException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/shell/GetConfigCommand.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.shell; 2 | 3 | import net.whistlingfish.harmony.HarmonyClient; 4 | 5 | public class GetConfigCommand extends ShellCommand { 6 | @Override 7 | public void execute(HarmonyClient harmonyClient) { 8 | println(harmonyClient.getConfig().toJson()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/shell/ShowActivityCommand.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.shell; 2 | 3 | import net.whistlingfish.harmony.HarmonyClient; 4 | import net.whistlingfish.harmony.config.Activity; 5 | 6 | public class ShowActivityCommand extends ShellCommand { 7 | @Override 8 | public void execute(HarmonyClient harmonyClient) { 9 | Activity activity = harmonyClient.getCurrentActivity(); 10 | println("%d: %s", activity.getId(), activity.getLabel()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/shell/ListDevicesCommand.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.shell; 2 | 3 | import java.util.Map.Entry; 4 | 5 | import net.whistlingfish.harmony.HarmonyClient; 6 | 7 | public class ListDevicesCommand extends ShellCommand { 8 | @Override 9 | public void execute(HarmonyClient harmonyClient) { 10 | for (Entry e : harmonyClient.getDeviceLabels().entrySet()) { 11 | println("%d: %s", e.getKey(), e.getValue()); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/protocol/AuthDetails.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.protocol; 2 | 3 | public class AuthDetails { 4 | private final String email; 5 | private final String password; 6 | 7 | public AuthDetails(String username, String password) { 8 | this.email = username; 9 | this.password = password; 10 | } 11 | 12 | public String getEmail() { 13 | return email; 14 | } 15 | 16 | public String getPassword() { 17 | return password; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/shell/ListActivitiesCommand.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.shell; 2 | 3 | import net.whistlingfish.harmony.HarmonyClient; 4 | import net.whistlingfish.harmony.config.Activity; 5 | 6 | public class ListActivitiesCommand extends ShellCommand { 7 | @Override 8 | public void execute(HarmonyClient harmonyClient) { 9 | for (Activity activity : harmonyClient.getConfig().getActivities()) { 10 | println("%s: %s", activity.getId(), activity.getLabel()); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/Jackson.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | 5 | import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; 6 | import static com.fasterxml.jackson.databind.DeserializationFeature.READ_ENUMS_USING_TO_STRING; 7 | 8 | public class Jackson { 9 | public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() // 10 | .disable(FAIL_ON_UNKNOWN_PROPERTIES) 11 | .enable(READ_ENUMS_USING_TO_STRING); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/protocol/LoginToken.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.protocol; 2 | 3 | public class LoginToken { 4 | private final String accountId; 5 | private final String userAuthToken; 6 | 7 | public LoginToken(String username, String password) { 8 | this.accountId = username; 9 | this.userAuthToken = password; 10 | } 11 | 12 | public String getAccountId() { 13 | return accountId; 14 | } 15 | 16 | public String getUserAuthToken() { 17 | return userAuthToken; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/ActivityChangeListener.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony; 2 | 3 | import net.whistlingfish.harmony.config.Activity; 4 | 5 | public abstract class ActivityChangeListener implements HarmonyHubListener { 6 | public abstract void activityStarted(Activity activity); 7 | 8 | @Override 9 | public void addTo(HarmonyClient harmonyClient) { 10 | harmonyClient.addListener(this); 11 | } 12 | 13 | @Override 14 | public void removeFrom(HarmonyClient harmonyClient) { 15 | harmonyClient.removeListener(this); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/config/Global.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.config; 2 | 3 | public class Global { 4 | private String timeStampHash; 5 | private String locale; 6 | 7 | public String getTimeStampHash() { 8 | return timeStampHash; 9 | } 10 | 11 | public void setTimeStampHash(String timeStampHash) { 12 | this.timeStampHash = timeStampHash; 13 | } 14 | 15 | public String getLocale() { 16 | return locale; 17 | } 18 | 19 | public void setLocale(String locale) { 20 | this.locale = locale; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/shell/ShellCommand.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.shell; 2 | 3 | import static java.lang.String.format; 4 | import static java.lang.System.out; 5 | 6 | import net.whistlingfish.harmony.HarmonyClient; 7 | 8 | public abstract class ShellCommand { 9 | public abstract void execute(HarmonyClient harmonyClient); 10 | 11 | protected void println(String fmt, Object... args) { 12 | if (args.length == 0) { 13 | out.println(fmt); 14 | } else { 15 | out.println(format(fmt, args)); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/config/ControlGroup.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.config; 2 | 3 | import java.util.List; 4 | 5 | public class ControlGroup { 6 | private String name; 7 | private List function; 8 | 9 | public String getName() { 10 | return name; 11 | } 12 | 13 | public void setName(String name) { 14 | this.name = name; 15 | } 16 | 17 | public List getFunction() { 18 | return function; 19 | } 20 | 21 | public void setFunction(List function) { 22 | this.function = function; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/ActivityStatusListener.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony; 2 | 3 | import net.whistlingfish.harmony.config.Activity; 4 | import net.whistlingfish.harmony.config.Activity.Status; 5 | 6 | public abstract class ActivityStatusListener implements HarmonyHubListener { 7 | public abstract void activityStatusChanged(Activity activity, Status status); 8 | 9 | @Override 10 | public void addTo(HarmonyClient harmonyClient) { 11 | harmonyClient.addListener(this); 12 | } 13 | 14 | @Override 15 | public void removeFrom(HarmonyClient harmonyClient) { 16 | harmonyClient.removeListener(this); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/shell/StartActivityCommand.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.shell; 2 | 3 | import net.whistlingfish.harmony.HarmonyClient; 4 | 5 | import org.kohsuke.args4j.Argument; 6 | 7 | public class StartActivityCommand extends ShellCommand { 8 | @Argument(required = true) 9 | private String activity; 10 | 11 | @Override 12 | public void execute(HarmonyClient harmonyClient) { 13 | try { 14 | harmonyClient.startActivity(Integer.parseInt(activity)); 15 | } catch (NumberFormatException e) { 16 | harmonyClient.startActivityByName(activity); 17 | } 18 | println("Activity %s started", activity); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/shell/ShowCommand.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.shell; 2 | 3 | import net.whistlingfish.harmony.HarmonyClient; 4 | 5 | import org.kohsuke.args4j.Argument; 6 | import org.kohsuke.args4j.spi.SubCommand; 7 | import org.kohsuke.args4j.spi.SubCommandHandler; 8 | import org.kohsuke.args4j.spi.SubCommands; 9 | 10 | public class ShowCommand extends ShellCommand { 11 | @Argument(handler = SubCommandHandler.class) 12 | @SubCommands({ @SubCommand(name = "activity", impl = ShowActivityCommand.class) }) 13 | private ShellCommand command; 14 | 15 | @Override 16 | public void execute(HarmonyClient harmonyClient) { 17 | command.execute(harmonyClient); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/config/Function.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.config; 2 | 3 | public class Function { 4 | private String name; 5 | private String label; 6 | private String action; 7 | 8 | public String getName() { 9 | return name; 10 | } 11 | 12 | public void setName(String name) { 13 | this.name = name; 14 | } 15 | 16 | public String getLabel() { 17 | return label; 18 | } 19 | 20 | public void setLabel(String label) { 21 | this.label = label; 22 | } 23 | 24 | public String getAction() { 25 | return action; 26 | } 27 | 28 | public void setAction(String action) { 29 | this.action = action; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/shell/PressButtonCommand.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.shell; 2 | 3 | import net.whistlingfish.harmony.HarmonyClient; 4 | 5 | import org.kohsuke.args4j.Argument; 6 | 7 | public class PressButtonCommand extends ShellCommand { 8 | @Argument(required = true, index = 0) 9 | private String deviceId; 10 | 11 | @Argument(required = true, index = 1) 12 | private String button; 13 | 14 | @Override 15 | public void execute(HarmonyClient harmonyClient) { 16 | try { 17 | harmonyClient.pressButton(Integer.parseInt(deviceId), button); 18 | } catch (NumberFormatException e) { 19 | harmonyClient.pressButton(deviceId, button); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/shell/ListCommand.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.shell; 2 | 3 | import net.whistlingfish.harmony.HarmonyClient; 4 | 5 | import org.kohsuke.args4j.Argument; 6 | import org.kohsuke.args4j.spi.SubCommand; 7 | import org.kohsuke.args4j.spi.SubCommandHandler; 8 | import org.kohsuke.args4j.spi.SubCommands; 9 | 10 | public class ListCommand extends ShellCommand { 11 | @Argument(handler = SubCommandHandler.class) 12 | @SubCommands({ @SubCommand(name = "devices", impl = ListDevicesCommand.class), 13 | @SubCommand(name = "activities", impl = ListActivitiesCommand.class)}) 14 | private ShellCommand command; 15 | 16 | @Override 17 | public void execute(HarmonyClient harmonyClient) { 18 | command.execute(harmonyClient); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | harmony-java-client (https://github.com/tuck182/harmony-java-client/) 2 | Copyright (c) 2014, 2015 Matt Tucker 3 | 4 | harmony-java-client is free software: you can redistribute it and/or modify 5 | it under the terms of either: 6 | 7 | GNU General Public License version 3, as published by the Free Software Foundation 8 | or 9 | Eclipse Public License - v 1.0, as published by The Eclipse Foundation 10 | 11 | You should have received a copy of at least one of the aforementioned licenses along with 12 | harmony-java-client. If not, see or 13 | . 14 | 15 | harmony-java-client is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 18 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/protocol/AuthFailedException.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.protocol; 2 | 3 | import static java.lang.String.format; 4 | 5 | public class AuthFailedException extends RuntimeException { 6 | private static final String DEFAULT_MESSAGE = "Logitech authentication failed"; 7 | private static final long serialVersionUID = 1L; 8 | 9 | public AuthFailedException() { 10 | super(DEFAULT_MESSAGE); 11 | } 12 | 13 | public AuthFailedException(String message) { 14 | super(format("%s: %s", DEFAULT_MESSAGE, message)); 15 | } 16 | 17 | public AuthFailedException(Throwable cause) { 18 | this(DEFAULT_MESSAGE, cause); 19 | } 20 | 21 | public AuthFailedException(String message, Throwable cause) { 22 | super(format("%s: %s", DEFAULT_MESSAGE, message), cause); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/HarmonyClientModule.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony; 2 | 3 | import java.io.IOException; 4 | 5 | import org.jivesoftware.smack.provider.ProviderFileLoader; 6 | import org.jivesoftware.smack.provider.ProviderManager; 7 | import org.jivesoftware.smack.util.FileUtils; 8 | 9 | import com.google.inject.Binder; 10 | import com.google.inject.Module; 11 | 12 | public class HarmonyClientModule implements Module { 13 | @Override 14 | public void configure(Binder binder) { 15 | try { 16 | ProviderManager.addLoader(new ProviderFileLoader(FileUtils.getStreamForUrl( 17 | "classpath:net/whistlingfish/harmony/smack-providers.xml", null))); 18 | } catch (IOException e) { 19 | throw new RuntimeException("Failed to initialize smack providers", e); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/protocol/IrCommand.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.protocol; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.google.common.collect.ImmutableMap; 5 | 6 | import static net.whistlingfish.harmony.Jackson.OBJECT_MAPPER; 7 | 8 | public abstract class IrCommand extends OAStanza { 9 | public IrCommand(String mimeType) { 10 | super(mimeType); 11 | } 12 | 13 | public String generateAction(int deviceId, String button) { 14 | try { 15 | return OBJECT_MAPPER.writeValueAsString(ImmutableMap. builder() // 16 | .put("type", "IRCommand") 17 | .put("deviceId", Integer.valueOf(deviceId).toString()) 18 | .put("command", button) 19 | .build()).replaceAll(":", "::"); 20 | } catch (JsonProcessingException e) { 21 | throw new RuntimeException(e); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/whistlingfish/harmony/shell/ShellCommandWrapper.java: -------------------------------------------------------------------------------- 1 | package net.whistlingfish.harmony.shell; 2 | 3 | import net.whistlingfish.harmony.HarmonyClient; 4 | 5 | import org.kohsuke.args4j.Argument; 6 | import org.kohsuke.args4j.spi.SubCommand; 7 | import org.kohsuke.args4j.spi.SubCommandHandler; 8 | import org.kohsuke.args4j.spi.SubCommands; 9 | 10 | public class ShellCommandWrapper { 11 | @Argument(handler = SubCommandHandler.class) 12 | @SubCommands({ @SubCommand(name = "get_config", impl = GetConfigCommand.class), 13 | @SubCommand(name = "press", impl = PressButtonCommand.class), 14 | @SubCommand(name = "start", impl = StartActivityCommand.class), 15 | @SubCommand(name = "list", impl = ListCommand.class), 16 | @SubCommand(name = "show", impl = ShowCommand.class), }) 17 | private ShellCommand command; 18 | 19 | public void execute(HarmonyClient harmonyClient) { 20 | command.execute(harmonyClient); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %msg%n 6 | 7 | 8 | TRACE 9 | DENY 10 | NEUTRAL 11 | 12 | 13 | 14 | 15 | System.err 16 | 17 | %msg%n 18 | 19 | 20 | TRACE 21 | NEUTRAL 22 | DENY 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | harmony-java-client 2 | =================== 3 | 4 | Java client for communicating with a Harmony Hub 5 | 6 | Based on the work done to reverse engineer and implement the Harmony Hub 7 | protocol in the following projects: 8 | * https://github.com/jterrace/pyharmony 9 | * https://github.com/petele/pyharmony 10 | * https://github.com/hdurdle/harmony 11 | 12 | The basics of the API are in place, and there's also a simple shell available 13 | through the Main class that demonstrates the API features. The available shell 14 | commands are: 15 | 16 | * list devices - lists the configured devices and their id's 17 | * list activities - lists the configured activities and their id's 18 | * show activity - shows the current activity 19 | * start \ - starts an activity (takes a string or id) 20 | * press \ \