/** * 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.myq.internal; import static org.openhab.io.net.http.HttpUtil.executeUrl; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Properties; import org.apache.commons.io.IOUtils; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.JsonProcessingException; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This Class handles the Chamberlain myQ http connection. * * <ul> * <li>userName: myQ Login Username</li> * <li>password: myQ Login Password</li> * <li>sercurityToken: sercurityToken for API requests</li> * <li>header: http header data</li> * <li>webSite: url of myQ API</li> * <li>appId: appId for API requests</li> * </ul> * * @author Scott Hanson * @author Dan Cunningham * @since 1.8.0 */ public class MyqData { static final Logger logger = LoggerFactory.getLogger(MyqData.class); private static final String WEBSITE = "https://myqexternal.myqdevice.com"; public static final String DEFAULT_APP_ID = "NWknvuBd7LoFHfXmKNMBcgajXtZEgKUh4V7WNzMidrpUUluDpVYVZx+xT4PCM5Kx"; private static final String CRAFTSMAN_WEBSITE = "https://craftexternal.myqdevice.com"; public static final String CRAFTSMAN_DEFAULT_APP_ID = "eU97d99kMG4t3STJZO/Mu2wt69yTQwM0WXZA5oZ74/ascQ2xQrLD/yjeVhEQccBZ"; public static final int DEFAUALT_TIMEOUT = 5000; private static final String CULTURE = "en"; private String userName; private String password; private String appId; private String brandId; private String websiteUrl; private int timeout; private String sercurityToken; private Properties header; /** * Constructor For Chamberlain MyQ http connection * * @param username * Chamberlain MyQ UserName * * @param password * Chamberlain MyQ password * * @param appId * Chamberlain Application Id, defaults to DEFAULT_APP_ID if null * * @param timeout * HTTP timeout in milliseconds, defaults to DEFAUALT_TIMEOUT if * not > 0 * * @param craftman * Use Craftman url instead of MyQ */ public MyqData(String username, String password, String appId, int timeout, boolean craftman) { this.userName = username; this.password = password; if (appId != null) { this.appId = appId; } else { if (craftman) { this.appId = CRAFTSMAN_DEFAULT_APP_ID; } else { this.appId = DEFAULT_APP_ID; } } if (craftman) { this.websiteUrl = CRAFTSMAN_WEBSITE; this.brandId = "3"; } else { this.websiteUrl = WEBSITE; this.brandId = "2"; } if (timeout > 0) { this.timeout = timeout; } else { this.timeout = DEFAUALT_TIMEOUT; } header = new Properties(); header.put("Accept", "application/json"); header.put("User-Agent", "Chamberlain/3.73"); header.put("BrandId", this.brandId); header.put("ApiVersion", "4.1"); header.put("Culture", CULTURE); header.put("MyQApplicationId", this.appId); } /** * Retrieves MyQ device data from myq website, throws if connection * fails or user login fails * */ public MyqDeviceData getMyqData() throws InvalidLoginException, IOException { logger.trace("Retrieving door data"); String url = String.format("%s/api/v4/userdevicedetails/get", websiteUrl); header.put("SecurityToken", getSecurityToken()); JsonNode data = request("GET", url, null, null, true); return new MyqDeviceData(data); } /** * Validates Username and Password then saved sercurityToken to a variable */ private void login() throws InvalidLoginException, IOException { logger.trace("attempting to login"); String url = String.format("%s/api/v4/User/Validate", websiteUrl); String message = String.format( "{\"username\":\"%s\",\"password\":\"%s\"}", userName, password); JsonNode data = request("POST", url, message,"application/json", true); LoginData login = new LoginData(data); sercurityToken = login.getSecurityToken(); } /** * Send Command to open/close garage door opener with MyQ API Returns false * if return code from API is not correct or connection fails * * @param deviceID * MyQ deviceID of Garage Door Opener. * * @param name * Attribute Name "desireddoorstate" or "desiredlightstate" * * @param state * Desired state to put the door in, 1 = open, 0 = closed * Desired state to put the lamp in, 1 = on, 0 = off */ public void executeMyQCommand(int deviceID, String name, int state) throws InvalidLoginException, IOException { String message = String.format( "{\"ApplicationId\":\"%s\"," + "\"SecurityToken\":\"%s\"," + "\"MyQDeviceId\":\"%d\"," + "\"AttributeName\":\"%s\"," + "\"AttributeValue\":\"%d\"}", appId, sercurityToken, deviceID, name, state); String url = String.format("%s/api/v4/DeviceAttribute/PutDeviceAttribute", websiteUrl); header.put("SecurityToken", getSecurityToken()); request("PUT", url, message, "application/json", true); } /** * Returns the currently cached security token, this will make a call to * login if the token does not exist. * * @return The cached security token * @throws IOException * @throws InvalidLoginException */ private String getSecurityToken() throws IOException, InvalidLoginException { if (sercurityToken == null) { login(); } return sercurityToken; } /** * Make a request to the server, optionally retry the call if there is a * login issue. Will throw a InvalidLoginExcpetion if the account is * invalid, locked or soon to be locked. * * @param method * The Http Method Type (GET,PUT) * @param url * The request URL * @param payload * Payload string for put operations * @param payloadType * Payload content type for put operations * @param retry * Retry the attempt if our session key is not valid * @return The JsonNode representing the response data * @throws IOException * @throws InvalidLoginException */ private synchronized JsonNode request(String method, String url, String payload, String payloadType, boolean retry) throws IOException, InvalidLoginException { logger.trace("Requesting URL {}", url); String dataString = executeUrl(method, url, header, payload == null ? null : IOUtils.toInputStream(payload), payloadType, timeout); logger.trace("Received MyQ JSON: {}", dataString); if (dataString == null) { throw new IOException("Null response from MyQ server"); } try { ObjectMapper mapper = new ObjectMapper(); JsonNode rootNode = mapper.readTree(dataString); int returnCode = rootNode.get("ReturnCode").asInt(); logger.trace("myq ReturnCode: {}", returnCode); MyQResponseCode rc = MyQResponseCode.fromCode(returnCode); switch (rc) { case OK: { return rootNode; } case ACCOUNT_INVALID: case ACCOUNT_NOT_FOUND: case ACCOUNT_LOCKED: case ACCOUNT_LOCKED_PENDING: // these are bad, we do not want to continue to log in and // lock an account throw new InvalidLoginException(rc.getDesc()); case LOGIN_ERROR: // Our session key has expired, request a new one if (retry) { login(); return request(method, url, payload, payloadType, false); } // fall through to default default: throw new IOException("Request Failed: " + rc.getDesc()); } } catch (JsonProcessingException e) { throw new IOException("Could not parse response", e); } } /** * URL Encode a string using UTF-8 encoding * * @param string * @return */ private String enc(String string) { try { return URLEncoder.encode(string, "UTF-8"); } catch (UnsupportedEncodingException e) { logger.warn("Could not encode string", e); return string; } } }