├── 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 \ \