[myq] Fixes breaking API changes to the MyQ binding (#11601)

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
This commit is contained in:
Dan Cunningham 2021-11-19 15:17:27 -08:00 committed by GitHub
parent 163a34fca4
commit d0837ae8a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 113 additions and 247 deletions

View File

@ -14,11 +14,12 @@ package org.openhab.binding.myq.internal;
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;
@ -55,9 +56,9 @@ public class MyQDiscoveryService extends AbstractDiscoveryService implements Dis
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(),
@ -73,6 +74,11 @@ public class MyQDiscoveryService extends AbstractDiscoveryService implements Dis
}
}
@Override
public void startBackgroundDiscovery() {
startScan();
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof MyQAccountHandler) {

View File

@ -18,21 +18,7 @@ package org.openhab.binding.myq.internal.dto;
* @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;
}

View File

@ -12,13 +12,13 @@
*/
package org.openhab.binding.myq.internal.dto;
import java.util.List;
/**
* The {@link AccountInfoDTO} entity from the MyQ API
* The {@link AccountsDTO} entity from the MyQ API
*
* @author Dan Cunningham - Initial contribution
*/
public class AccountInfoDTO {
public String href;
public String id;
public class AccountsDTO {
public List<AccountDTO> accounts;
}

View File

@ -1,28 +0,0 @@
/**
* 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;
}

View File

@ -1,26 +0,0 @@
/**
* 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;
}

View File

@ -1,25 +0,0 @@
/**
* 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;
}

View File

@ -25,6 +25,7 @@ public class DeviceDTO {
public String deviceType;
public String name;
public String createdDate;
public String accountId;
public DeviceStateDTO state;
public String parentDevice;
public String parentDeviceId;

View File

@ -12,8 +12,6 @@
*/
package org.openhab.binding.myq.internal.dto;
import java.util.List;
/**
* The {@link DeviceStateDTO} entity from the MyQ API
*
@ -23,34 +21,16 @@ public class DeviceStateDTO {
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;
}

View File

@ -1,24 +0,0 @@
/**
* 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;
}

View File

@ -1,23 +0,0 @@
/**
* 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;
}

View File

@ -23,10 +23,12 @@ import java.net.URISyntaxException;
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;
@ -47,7 +49,6 @@ import org.eclipse.jetty.client.api.Response;
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;
@ -57,7 +58,8 @@ import org.jsoup.nodes.Element;
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;
@ -106,20 +108,23 @@ public class MyQAccountHandler extends BaseBridgeHandler implements AccessTokenR
// 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;
@ -155,6 +160,7 @@ public class MyQAccountHandler extends BaseBridgeHandler implements AccessTokenR
@Override
public void dispose() {
stopPolls();
OAuthClientService oAuthService = this.oAuthService;
if (oAuthService != null) {
oAuthService.close();
}
@ -167,10 +173,10 @@ public class MyQAccountHandler extends BaseBridgeHandler implements AccessTokenR
@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);
}
@ -182,33 +188,42 @@ public class MyQAccountHandler extends BaseBridgeHandler implements AccessTokenR
}
/**
* Sends an action to the MyQ API
* Sends a door action to the MyQ API
*
* @param serialNumber
* @param device
* @param action
*/
public void sendAction(String serialNumber, String action) {
public void sendDoorAction(DeviceDTO device, String action) {
sendAction(device, action, CMD_DOOR_URL);
}
/**
* Sends a lamp action to the MyQ API
*
* @param device
* @param 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);
}
}
@ -217,7 +232,7 @@ public class MyQAccountHandler extends BaseBridgeHandler implements AccessTokenR
*
* @return cached MyQ devices
*/
public @Nullable DevicesDTO devicesCache() {
public @Nullable List<DeviceDTO> devicesCache() {
return devicesCache;
}
@ -266,8 +281,8 @@ public class MyQAccountHandler extends BaseBridgeHandler implements AccessTokenR
private synchronized void fetchData() {
try {
if (account == null) {
getAccount();
if (accounts == null) {
getAccounts();
}
getDevices();
} catch (MyQCommunicationException e) {
@ -338,33 +353,37 @@ public class MyQAccountHandler extends BaseBridgeHandler implements AccessTokenR
}
}
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,

View File

@ -40,7 +40,7 @@ import org.openhab.core.types.UnDefType;
*/
@NonNullByDefault
public class MyQGarageDoorHandler extends BaseThingHandler implements MyQDeviceHandler {
private @Nullable DeviceDTO deviceState;
private @Nullable DeviceDTO device;
private String serialNumber;
public MyQGarageDoorHandler(Thing thing) {
@ -60,8 +60,8 @@ public class MyQGarageDoorHandler extends BaseThingHandler implements MyQDeviceH
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;
@ -78,7 +78,7 @@ public class MyQGarageDoorHandler extends BaseThingHandler implements MyQDeviceH
cmd = command.toString();
}
if (cmd != null) {
((MyQAccountHandler) handler).sendAction(localState.serialNumber, cmd);
((MyQAccountHandler) handler).sendDoorAction(localDevice, cmd);
}
}
}
@ -90,9 +90,9 @@ public class MyQGarageDoorHandler extends BaseThingHandler implements MyQDeviceH
}
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":
@ -112,8 +112,8 @@ public class MyQGarageDoorHandler extends BaseThingHandler implements MyQDeviceH
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);
}
}
@ -122,7 +122,7 @@ public class MyQGarageDoorHandler extends BaseThingHandler implements MyQDeviceH
if (!MyQBindingConstants.THING_TYPE_GARAGEDOOR.getId().equals(device.deviceFamily)) {
return;
}
deviceState = device;
this.device = device;
if (device.state.online) {
updateStatus(ThingStatus.ONLINE);
updateState();

View File

@ -36,7 +36,7 @@ import org.openhab.core.types.RefreshType;
*/
@NonNullByDefault
public class MyQLampHandler extends BaseThingHandler implements MyQDeviceHandler {
private @Nullable DeviceDTO deviceState;
private @Nullable DeviceDTO device;
private String serialNumber;
public MyQLampHandler(Thing thing) {
@ -58,11 +58,11 @@ public class MyQLampHandler extends BaseThingHandler implements MyQDeviceHandler
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");
}
}
@ -75,9 +75,9 @@ public class MyQLampHandler extends BaseThingHandler implements MyQDeviceHandler
}
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);
}
}
@ -87,7 +87,7 @@ public class MyQLampHandler extends BaseThingHandler implements MyQDeviceHandler
if (!MyQBindingConstants.THING_TYPE_LAMP.getId().equals(device.deviceFamily)) {
return;
}
deviceState = device;
this.device = device;
if (device.state.online) {
updateStatus(ThingStatus.ONLINE);
updateState();