import static org.openhab.binding.myq.internal.MyQBindingConstants.BINDING_ID;
+import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.myq.internal.dto.DevicesDTO;
+import org.openhab.binding.myq.internal.dto.DeviceDTO;
import org.openhab.binding.myq.internal.handler.MyQAccountHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
public void startScan() {
MyQAccountHandler accountHandler = this.accountHandler;
if (accountHandler != null) {
- DevicesDTO devices = accountHandler.devicesCache();
+ List<DeviceDTO> devices = accountHandler.devicesCache();
if (devices != null) {
- devices.items.forEach(device -> {
+ devices.forEach(device -> {
ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, device.deviceFamily);
if (SUPPORTED_DISCOVERY_THING_TYPES_UIDS.contains(thingTypeUID)) {
ThingUID thingUID = new ThingUID(thingTypeUID, accountHandler.getThing().getUID(),
}
}
+ @Override
+ public void startBackgroundDiscovery() {
+ startScan();
+ }
+
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof MyQAccountHandler) {
* @author Dan Cunningham - Initial contribution
*/
public class AccountDTO {
-
- public UsersDTO users;
- public Boolean admin;
- public AccountInfoDTO account;
- public String analyticsId;
- public String userId;
- public String userName;
- public String email;
- public String firstName;
- public String lastName;
- public String cultureCode;
- public AddressDTO address;
- public TimeZoneDTO timeZone;
- public Boolean mailingListOptIn;
- public Boolean requestAccountLinkInfo;
- public String phone;
- public Boolean diagnosticDataOptIn;
+ public String id;
+ public String name;
+ public String createdBy;
}
+++ /dev/null
-/**
- * Copyright (c) 2010-2021 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.myq.internal.dto;
-
-/**
- * The {@link AccountInfoDTO} entity from the MyQ API
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class AccountInfoDTO {
-
- public String href;
- public String id;
-}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.myq.internal.dto;
+
+import java.util.List;
+
+/**
+ * The {@link AccountsDTO} entity from the MyQ API
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class AccountsDTO {
+ public List<AccountDTO> accounts;
+}
+++ /dev/null
-/**
- * Copyright (c) 2010-2021 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.myq.internal.dto;
-
-/**
- * The {@link ActionDTO} entity from the MyQ API
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class ActionDTO {
-
- public ActionDTO(String actionType) {
- super();
- this.actionType = actionType;
- }
-
- public String actionType;
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2021 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.myq.internal.dto;
-
-/**
- * The {@link AddressDTO} entity from the MyQ API
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class AddressDTO {
-
- public String addressLine1;
- public String city;
- public String postalCode;
- public CountryDTO country;
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2021 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.myq.internal.dto;
-
-/**
- * The {@link CountryDTO} entity from the MyQ API
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class CountryDTO {
-
- public String code;
- public Boolean isEEACountry;
- public String href;
-}
public String deviceType;
public String name;
public String createdDate;
+ public String accountId;
public DeviceStateDTO state;
public String parentDevice;
public String parentDeviceId;
*/
package org.openhab.binding.myq.internal.dto;
-import java.util.List;
-
/**
* The {@link DeviceStateDTO} entity from the MyQ API
*
public Boolean gdoLockConnected;
public Boolean attachedWorkLightErrorPresent;
- public String doorState;
+ public String learnStatus;
+ public Boolean hasCamera;
public String lampState;
- public String open;
- public String close;
+ public String batteryBackupState;
+ public String doorState;
public String lastUpdate;
- public String passthroughInterval;
- public String doorAjarInterval;
- public String invalidCredentialWindow;
- public String invalidShutoutPeriod;
public Boolean isUnattendedOpenAllowed;
public Boolean isUnattendedCloseAllowed;
- public String auxRelayDelay;
- public Boolean useAuxRelay;
- public String auxRelayBehavior;
- public Boolean rexFiresDoor;
- public Boolean commandChannelReportStatus;
- public Boolean controlFromBrowser;
- public Boolean reportForced;
- public Boolean reportAjar;
- public Integer maxInvalidAttempts;
+ public Integer serviceCycleCount;
+ public Integer absoluteCycleCount;
public Boolean online;
public String lastStatus;
- public String firmwareVersion;
- public Boolean homekitCapable;
- public Boolean homekitEnabled;
- public String learn;
- public Boolean learnMode;
- public String updatedDate;
- public List<String> physicalDevices = null;
- public Boolean pendingBootloadAbandoned;
}
+++ /dev/null
-/**
- * Copyright (c) 2010-2021 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.myq.internal.dto;
-
-/**
- * The {@link TimeZoneDTO} entity from the MyQ API
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class TimeZoneDTO {
-
- public String id;
- public String name;
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2021 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.myq.internal.dto;
-
-/**
- * The {@link UsersDTO} entity from the MyQ API
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class UsersDTO {
-
- public String href;
-}
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.FormContentProvider;
-import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.Fields;
import org.openhab.binding.myq.internal.MyQDiscoveryService;
import org.openhab.binding.myq.internal.config.MyQAccountConfiguration;
import org.openhab.binding.myq.internal.dto.AccountDTO;
-import org.openhab.binding.myq.internal.dto.ActionDTO;
+import org.openhab.binding.myq.internal.dto.AccountsDTO;
+import org.openhab.binding.myq.internal.dto.DeviceDTO;
import org.openhab.binding.myq.internal.dto.DevicesDTO;
import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
// this should never happen, but lets be safe and give up after so many redirects
private static final int LOGIN_MAX_REDIRECTS = 30;
/*
- * MyQ device and account API endpoint
+ * MyQ device and account API endpoints
*/
- private static final String BASE_URL = "https://api.myqdevice.com/api";
+ private static final String ACCOUNTS_URL = "https://accounts.myq-cloud.com/api/v6.0/accounts";
+ private static final String DEVICES_URL = "https://devices.myq-cloud.com/api/v5.2/Accounts/%s/Devices";
+ private static final String CMD_LAMP_URL = "https://account-devices-lamp.myq-cloud.com/api/v5.2/Accounts/%s/lamps/%s/%s";
+ private static final String CMD_DOOR_URL = "https://account-devices-gdo.myq-cloud.com/api/v5.2/Accounts/%s/door_openers/%s/%s";
+
private static final Integer RAPID_REFRESH_SECONDS = 5;
private final Logger logger = LoggerFactory.getLogger(MyQAccountHandler.class);
- private final Gson gsonUpperCase = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
- .create();
private final Gson gsonLowerCase = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
private final OAuthFactory oAuthFactory;
private @Nullable Future<?> normalPollFuture;
private @Nullable Future<?> rapidPollFuture;
- private @Nullable AccountDTO account;
- private @Nullable DevicesDTO devicesCache;
+ private @Nullable AccountsDTO accounts;
+
+ private List<DeviceDTO> devicesCache = new ArrayList<DeviceDTO>();
private @Nullable OAuthClientService oAuthService;
private Integer normalRefreshSeconds = 60;
private HttpClient httpClient;
@Override
public void dispose() {
stopPolls();
+ OAuthClientService oAuthService = this.oAuthService;
if (oAuthService != null) {
oAuthService.close();
}
@Override
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
- DevicesDTO localDeviceCaches = devicesCache;
- if (localDeviceCaches != null && childHandler instanceof MyQDeviceHandler) {
+ List<DeviceDTO> localDeviceCaches = devicesCache;
+ if (childHandler instanceof MyQDeviceHandler) {
MyQDeviceHandler handler = (MyQDeviceHandler) childHandler;
- localDeviceCaches.items.stream()
+ localDeviceCaches.stream()
.filter(d -> ((MyQDeviceHandler) childHandler).getSerialNumber().equalsIgnoreCase(d.serialNumber))
.findFirst().ifPresent(handler::handleDeviceUpdate);
}
}
/**
- * Sends an action to the MyQ API
+ * Sends a door action to the MyQ API
+ *
+ * @param device
+ * @param action
+ */
+ public void sendDoorAction(DeviceDTO device, String action) {
+ sendAction(device, action, CMD_DOOR_URL);
+ }
+
+ /**
+ * Sends a lamp action to the MyQ API
*
- * @param serialNumber
+ * @param device
* @param action
*/
- public void sendAction(String serialNumber, String action) {
+ public void sendLampAction(DeviceDTO device, String action) {
+ sendAction(device, action, CMD_LAMP_URL);
+ }
+
+ private void sendAction(DeviceDTO device, String action, String urlFormat) {
if (getThing().getStatus() != ThingStatus.ONLINE) {
logger.debug("Account offline, ignoring action {}", action);
return;
}
- AccountDTO localAccount = account;
- if (localAccount != null) {
- try {
- ContentResponse response = sendRequest(
- String.format("%s/v5.1/Accounts/%s/Devices/%s/actions", BASE_URL, localAccount.account.id,
- serialNumber),
- HttpMethod.PUT, new StringContentProvider(gsonLowerCase.toJson(new ActionDTO(action))),
- "application/json");
- if (HttpStatus.isSuccess(response.getStatus())) {
- restartPolls(true);
- } else {
- logger.debug("Failed to send action {} : {}", action, response.getContentAsString());
- }
- } catch (InterruptedException | MyQCommunicationException | MyQAuthenticationException e) {
- logger.debug("Could not send action", e);
+ try {
+ ContentResponse response = sendRequest(
+ String.format(urlFormat, device.accountId, device.serialNumber, action), HttpMethod.PUT, null,
+ null);
+ if (HttpStatus.isSuccess(response.getStatus())) {
+ restartPolls(true);
+ } else {
+ logger.debug("Failed to send action {} : {}", action, response.getContentAsString());
}
+ } catch (InterruptedException | MyQCommunicationException | MyQAuthenticationException e) {
+ logger.debug("Could not send action", e);
}
}
*
* @return cached MyQ devices
*/
- public @Nullable DevicesDTO devicesCache() {
+ public @Nullable List<DeviceDTO> devicesCache() {
return devicesCache;
}
private synchronized void fetchData() {
try {
- if (account == null) {
- getAccount();
+ if (accounts == null) {
+ getAccounts();
}
getDevices();
} catch (MyQCommunicationException e) {
}
}
- private void getAccount() throws InterruptedException, MyQCommunicationException, MyQAuthenticationException {
- ContentResponse response = sendRequest(BASE_URL + "/v5/My?expand=account", HttpMethod.GET, null, null);
- account = parseResultAndUpdateStatus(response, gsonUpperCase, AccountDTO.class);
+ private void getAccounts() throws InterruptedException, MyQCommunicationException, MyQAuthenticationException {
+ ContentResponse response = sendRequest(ACCOUNTS_URL, HttpMethod.GET, null, null);
+ accounts = parseResultAndUpdateStatus(response, gsonLowerCase, AccountsDTO.class);
}
private void getDevices() throws InterruptedException, MyQCommunicationException, MyQAuthenticationException {
- AccountDTO localAccount = account;
- if (localAccount == null) {
+ AccountsDTO localAccounts = accounts;
+ if (localAccounts == null) {
return;
}
- ContentResponse response = sendRequest(
- String.format("%s/v5.1/Accounts/%s/Devices", BASE_URL, localAccount.account.id), HttpMethod.GET, null,
- null);
- DevicesDTO devices = parseResultAndUpdateStatus(response, gsonLowerCase, DevicesDTO.class);
- devicesCache = devices;
- devices.items.forEach(device -> {
- ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, device.deviceFamily);
- if (SUPPORTED_DISCOVERY_THING_TYPES_UIDS.contains(thingTypeUID)) {
- for (Thing thing : getThing().getThings()) {
- ThingHandler handler = thing.getHandler();
- if (handler != null
- && ((MyQDeviceHandler) handler).getSerialNumber().equalsIgnoreCase(device.serialNumber)) {
- ((MyQDeviceHandler) handler).handleDeviceUpdate(device);
+
+ List<DeviceDTO> currentDevices = new ArrayList<DeviceDTO>();
+
+ for (AccountDTO account : localAccounts.accounts) {
+ ContentResponse response = sendRequest(String.format(DEVICES_URL, account.id), HttpMethod.GET, null, null);
+ DevicesDTO devices = parseResultAndUpdateStatus(response, gsonLowerCase, DevicesDTO.class);
+ currentDevices.addAll(devices.items);
+ devices.items.forEach(device -> {
+ ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, device.deviceFamily);
+ if (SUPPORTED_DISCOVERY_THING_TYPES_UIDS.contains(thingTypeUID)) {
+ for (Thing thing : getThing().getThings()) {
+ ThingHandler handler = thing.getHandler();
+ if (handler != null && ((MyQDeviceHandler) handler).getSerialNumber()
+ .equalsIgnoreCase(device.serialNumber)) {
+ ((MyQDeviceHandler) handler).handleDeviceUpdate(device);
+ }
}
}
- }
- });
+ });
+ }
+ devicesCache = currentDevices;
}
private synchronized ContentResponse sendRequest(String url, HttpMethod method, @Nullable ContentProvider content,
*/
@NonNullByDefault
public class MyQGarageDoorHandler extends BaseThingHandler implements MyQDeviceHandler {
- private @Nullable DeviceDTO deviceState;
+ private @Nullable DeviceDTO device;
private String serialNumber;
public MyQGarageDoorHandler(Thing thing) {
return;
}
Bridge bridge = getBridge();
- final DeviceDTO localState = deviceState;
- if (bridge != null && localState != null) {
+ final DeviceDTO localDevice = device;
+ if (bridge != null && localDevice != null) {
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
String cmd = null;
cmd = command.toString();
}
if (cmd != null) {
- ((MyQAccountHandler) handler).sendAction(localState.serialNumber, cmd);
+ ((MyQAccountHandler) handler).sendDoorAction(localDevice, cmd);
}
}
}
}
protected void updateState() {
- final DeviceDTO localState = deviceState;
- if (localState != null) {
- String doorState = localState.state.doorState;
+ final DeviceDTO localDevice = device;
+ if (localDevice != null) {
+ String doorState = localDevice.state.doorState;
updateState("status", new StringType(doorState));
switch (doorState) {
case "open":
updateState("rollershutter", UnDefType.UNDEF);
break;
}
- updateState("closeerror", localState.state.isUnattendedCloseAllowed ? OnOffType.OFF : OnOffType.ON);
- updateState("openerror", localState.state.isUnattendedOpenAllowed ? OnOffType.OFF : OnOffType.ON);
+ updateState("closeerror", localDevice.state.isUnattendedCloseAllowed ? OnOffType.OFF : OnOffType.ON);
+ updateState("openerror", localDevice.state.isUnattendedOpenAllowed ? OnOffType.OFF : OnOffType.ON);
}
}
if (!MyQBindingConstants.THING_TYPE_GARAGEDOOR.getId().equals(device.deviceFamily)) {
return;
}
- deviceState = device;
+ this.device = device;
if (device.state.online) {
updateStatus(ThingStatus.ONLINE);
updateState();
*/
@NonNullByDefault
public class MyQLampHandler extends BaseThingHandler implements MyQDeviceHandler {
- private @Nullable DeviceDTO deviceState;
+ private @Nullable DeviceDTO device;
private String serialNumber;
public MyQLampHandler(Thing thing) {
if (command instanceof OnOffType) {
Bridge bridge = getBridge();
- final DeviceDTO localState = deviceState;
- if (bridge != null && localState != null) {
+ final DeviceDTO localDevice = device;
+ if (bridge != null && localDevice != null) {
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
- ((MyQAccountHandler) handler).sendAction(localState.serialNumber,
+ ((MyQAccountHandler) handler).sendLampAction(localDevice,
command == OnOffType.ON ? "turnon" : "turnoff");
}
}
}
protected void updateState() {
- final DeviceDTO localState = deviceState;
- if (localState != null) {
- String lampState = localState.state.lampState;
+ final DeviceDTO localDevice = device;
+ if (localDevice != null) {
+ String lampState = localDevice.state.lampState;
updateState("switch", "on".equals(lampState) ? OnOffType.ON : OnOffType.OFF);
}
}
if (!MyQBindingConstants.THING_TYPE_LAMP.getId().equals(device.deviceFamily)) {
return;
}
- deviceState = device;
+ this.device = device;
if (device.state.online) {
updateStatus(ThingStatus.ONLINE);
updateState();