[MyQ] Initial commit of the MyQ binding for OH3 (#9347)

* Rebase with main, update license headers
* Small PR cleanups
* One last small PR cleanup
* Syntactical sugar
* Updated error handling
* Spelling mistake

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
This commit is contained in:
Dan Cunningham 2021-02-26 14:50:25 -08:00 committed by GitHub
parent fdc22c0a4c
commit 42edf53a5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1436 additions and 0 deletions

View File

@ -171,6 +171,7 @@
/bundles/org.openhab.binding.mqtt.generic/ @davidgraeff
/bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff
/bundles/org.openhab.binding.mqtt.homie/ @davidgraeff
/bundles/org.openhab.binding.myq/ @digitaldan
/bundles/org.openhab.binding.mystrom/ @pail23
/bundles/org.openhab.binding.nanoleaf/ @raepple @stefan-hoehn
/bundles/org.openhab.binding.neato/ @jjlauterbach

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,68 @@
# MyQ Binding
This binding integrates with the [The Chamberlain Group MyQ](https://www.myq.com) cloud service. It allows monitoring and control over [MyQ](https://www.myq.com) enabled garage doors manufactured by LiftMaster, Chamberlain and Craftsman.
## Supported Things
### Account
This represents the MyQ cloud account and uses the same credentials needed when using the MyQ mobile application.
ThingTypeUID: `account`
### Garage Door
This represents a garage door associated with an account. Multiple garage doors are supported.
ThingTypeUID: `garagedoor`
### Lamp
This represents a lamp associated with an account. Multiple lamps are supported.
ThingTypeUID: `lamp`
## Discovery
Once an account has been added, garage doors and lamps will automatically be discovered and added to the inbox.
## Channels
| Channel | Item Type | Thing Type | States |
|---------------|---------------|------------------|--------------------------------------------------------|
| status | String | garagedoor | opening, closed, closing, stopped, transition, unknown |
| rollershutter | Rollershutter | garagedoor | UP, DOWN, 0%, 100% |
| switch | Switch | garagedoor, lamp | ON (open), OFF (closed)
## Full Example
### Thing Configuration
```xtend
Bridge myq:account:home "MyQ Account" [ username="foo@bar.com", password="secret", refreshInterval=60 ] {
Thing garagedoor abcd12345 "MyQ Garage Door" [ serialNumber="abcd12345" ]
Thing lamp efgh6789 "MyQ Lamp" [ serialNumber="efgh6789" ]
}
```
### Items
```xtend
String MyQGarageDoor1Status "Door Status [%s]" {channel = "myq:garagedoor:home:abcd12345:status"}
Switch MyQGarageDoor1Switch "Door Switch [%s]" {channel = "myq:garagedoor:home:abcd12345:switch"}
Rollershutter MyQGarageDoor1Rollershutter "Door Rollershutter [%s]" {channel = "myq:garagedoor:home:abcd12345:rollershutter"}
Switch MyQGarageDoorLamp "Lamp [%s]" {channel = "myq:lamp:home:efgh6789:switch"}
}
```
### Sitemaps
```xtend
sitemap MyQ label="MyQ Demo Sitemap" {
Frame label="Garage Door" {
String item=MyQGarageDoor1Status
Switch item=MyQGarageDoor1Switch
Rollershutter item=MyQGarageDoor1Rollershutter
}
}
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.1.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.myq</artifactId>
<name>openHAB Add-ons :: Bundles :: MyQ Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.myq-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-myq" description="MyQ Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.myq/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,38 @@
/**
* 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;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link MyQBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class MyQBindingConstants {
public static final String BINDING_ID = "myq";
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_GARAGEDOOR = new ThingTypeUID(BINDING_ID, "garagedoor");
public static final ThingTypeUID THING_TYPE_LAMP = new ThingTypeUID(BINDING_ID, "lamp");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_GARAGEDOOR,
THING_TYPE_LAMP);
public static final Set<ThingTypeUID> SUPPORTED_DISCOVERY_THING_TYPES_UIDS = Set.of(THING_TYPE_GARAGEDOOR,
THING_TYPE_LAMP);
}

View File

@ -0,0 +1,97 @@
/**
* 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;
import static org.openhab.binding.myq.internal.MyQBindingConstants.BINDING_ID;
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.handler.MyQAccountHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
/**
* The {@link MyQDiscoveryService} is responsible for discovering MyQ things
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class MyQDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
private static final Set<ThingTypeUID> SUPPORTED_DISCOVERY_THING_TYPES_UIDS = Set
.of(MyQBindingConstants.THING_TYPE_GARAGEDOOR, MyQBindingConstants.THING_TYPE_LAMP);
private @Nullable MyQAccountHandler accountHandler;
public MyQDiscoveryService() {
super(SUPPORTED_DISCOVERY_THING_TYPES_UIDS, 1, true);
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_DISCOVERY_THING_TYPES_UIDS;
}
@Override
public void startScan() {
MyQAccountHandler accountHandler = this.accountHandler;
if (accountHandler != null) {
DevicesDTO devices = accountHandler.devicesCache();
if (devices != null) {
devices.items.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(),
device.serialNumber.toLowerCase());
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("MyQ " + device.name)
.withProperty(Thing.PROPERTY_SERIAL_NUMBER, thingUID.getId())
.withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER)
.withBridge(accountHandler.getThing().getUID()).build();
thingDiscovered(result);
}
});
}
}
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof MyQAccountHandler) {
accountHandler = (MyQAccountHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return accountHandler;
}
@Override
public void activate() {
super.activate(null);
}
@Override
public void deactivate() {
super.deactivate();
}
}

View File

@ -0,0 +1,73 @@
/**
* 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;
import static org.openhab.binding.myq.internal.MyQBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.myq.internal.handler.MyQAccountHandler;
import org.openhab.binding.myq.internal.handler.MyQGarageDoorHandler;
import org.openhab.binding.myq.internal.handler.MyQLampHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link MyQHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.myq", service = ThingHandlerFactory.class)
public class MyQHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient;
@Activate
public MyQHandlerFactory(final @Reference HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
return new MyQAccountHandler((Bridge) thing, httpClient);
}
if (THING_TYPE_GARAGEDOOR.equals(thingTypeUID)) {
return new MyQGarageDoorHandler(thing);
}
if (THING_TYPE_LAMP.equals(thingTypeUID)) {
return new MyQLampHandler(thing);
}
return null;
}
}

View File

@ -0,0 +1,27 @@
/**
* 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.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link MyQAccountConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class MyQAccountConfiguration {
public String username = "";
public String password = "";
public Integer refreshInterval = 60;
}

View File

@ -0,0 +1,25 @@
/**
* 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.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link MyQDeviceConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class MyQDeviceConfiguration {
public String serialNumber = "";
}

View File

@ -0,0 +1,38 @@
/**
* 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 AccountDTO} entity from the MyQ API
*
* @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;
}

View File

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

View File

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

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

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

@ -0,0 +1,31 @@
/**
* 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 DeviceDTO} entity from the MyQ API
*
* @author Dan Cunningham - Initial contribution
*/
public class DeviceDTO {
public String href;
public String serialNumber;
public String deviceFamily;
public String devicePlatform;
public String deviceType;
public String name;
public String createdDate;
public DeviceStateDTO state;
public String parentDevice;
public String parentDeviceId;
}

View File

@ -0,0 +1,56 @@
/**
* 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 DeviceStateDTO} entity from the MyQ API
*
* @author Dan Cunningham - Initial contribution
*/
public class DeviceStateDTO {
public Boolean gdoLockConnected;
public Boolean attachedWorkLightErrorPresent;
public String doorState;
public String lampState;
public String open;
public String close;
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 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

@ -0,0 +1,26 @@
/**
* 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 DevicesDTO} entity from the MyQ API
*
* @author Dan Cunningham - Initial contribution
*/
public class DevicesDTO {
public String href;
public Integer count;
public List<DeviceDTO> items;
}

View File

@ -0,0 +1,30 @@
/**
* 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 LoginRequestDTO} entity from the MyQ API
*
* @author Dan Cunningham - Initial contribution
*/
public class LoginRequestDTO {
public LoginRequestDTO(String username, String password) {
super();
this.username = username;
this.password = password;
}
public String username;
public String password;
}

View File

@ -0,0 +1,22 @@
/**
* 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 LoginResponseDTO} entity from the MyQ API
*
* @author Dan Cunningham - Initial contribution
*/
public class LoginResponseDTO {
public String securityToken;
}

View File

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

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

@ -0,0 +1,344 @@
/**
* 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.handler;
import static org.openhab.binding.myq.internal.MyQBindingConstants.*;
import java.util.Collection;
import java.util.Collections;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
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.DevicesDTO;
import org.openhab.binding.myq.internal.dto.LoginRequestDTO;
import org.openhab.binding.myq.internal.dto.LoginResponseDTO;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
/**
* The {@link MyQAccountHandler} is responsible for communicating with the MyQ API based on an account.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class MyQAccountHandler extends BaseBridgeHandler {
private static final String BASE_URL = "https://api.myqdevice.com/api";
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 @Nullable Future<?> normalPollFuture;
private @Nullable Future<?> rapidPollFuture;
private @Nullable String securityToken;
private @Nullable AccountDTO account;
private @Nullable DevicesDTO devicesCache;
private Integer normalRefreshSeconds = 60;
private HttpClient httpClient;
private String username = "";
private String password = "";
private String userAgent = "";
public MyQAccountHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.httpClient = httpClient;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void initialize() {
MyQAccountConfiguration config = getConfigAs(MyQAccountConfiguration.class);
normalRefreshSeconds = config.refreshInterval;
username = config.username;
password = config.password;
// MyQ can get picky about blocking user agents apparently
userAgent = MyQAccountHandler.randomString(40);
securityToken = null;
updateStatus(ThingStatus.UNKNOWN);
restartPolls(false);
}
@Override
public void dispose() {
stopPolls();
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(MyQDiscoveryService.class);
}
@Override
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
DevicesDTO localDeviceCaches = devicesCache;
if (localDeviceCaches != null && childHandler instanceof MyQDeviceHandler) {
MyQDeviceHandler handler = (MyQDeviceHandler) childHandler;
localDeviceCaches.items.stream()
.filter(d -> ((MyQDeviceHandler) childHandler).getSerialNumber().equalsIgnoreCase(d.serialNumber))
.findFirst().ifPresent(handler::handleDeviceUpdate);
}
}
/**
* Sends an action to the MyQ API
*
* @param serialNumber
* @param action
*/
public void sendAction(String serialNumber, String action) {
AccountDTO localAccount = account;
if (localAccount != null) {
try {
HttpResult result = sendRequest(
String.format("%s/v5.1/Accounts/%s/Devices/%s/actions", BASE_URL, localAccount.account.id,
serialNumber),
HttpMethod.PUT, securityToken,
new StringContentProvider(gsonLowerCase.toJson(new ActionDTO(action))), "application/json");
if (HttpStatus.isSuccess(result.responseCode)) {
restartPolls(true);
} else {
logger.debug("Failed to send action {} : {}", action, result.content);
}
} catch (InterruptedException e) {
}
}
}
/**
* Last known state of MyQ Devices
*
* @return cached MyQ devices
*/
public @Nullable DevicesDTO devicesCache() {
return devicesCache;
}
private void stopPolls() {
stopNormalPoll();
stopRapidPoll();
}
private synchronized void stopNormalPoll() {
stopFuture(normalPollFuture);
normalPollFuture = null;
}
private synchronized void stopRapidPoll() {
stopFuture(rapidPollFuture);
rapidPollFuture = null;
}
private void stopFuture(@Nullable Future<?> future) {
if (future != null) {
future.cancel(true);
}
}
private synchronized void restartPolls(boolean rapid) {
stopPolls();
if (rapid) {
normalPollFuture = scheduler.scheduleWithFixedDelay(this::normalPoll, 35, normalRefreshSeconds,
TimeUnit.SECONDS);
rapidPollFuture = scheduler.scheduleWithFixedDelay(this::rapidPoll, 3, RAPID_REFRESH_SECONDS,
TimeUnit.SECONDS);
} else {
normalPollFuture = scheduler.scheduleWithFixedDelay(this::normalPoll, 0, normalRefreshSeconds,
TimeUnit.SECONDS);
}
}
private void normalPoll() {
stopRapidPoll();
fetchData();
}
private void rapidPoll() {
fetchData();
}
private synchronized void fetchData() {
try {
if (securityToken == null) {
login();
if (securityToken != null) {
getAccount();
}
}
if (securityToken != null) {
getDevices();
}
} catch (InterruptedException e) {
}
}
private void login() throws InterruptedException {
HttpResult result = sendRequest(BASE_URL + "/v5/Login", HttpMethod.POST, null,
new StringContentProvider(gsonUpperCase.toJson(new LoginRequestDTO(username, password))),
"application/json");
LoginResponseDTO loginResponse = parseResultAndUpdateStatus(result, gsonUpperCase, LoginResponseDTO.class);
if (loginResponse != null) {
securityToken = loginResponse.securityToken;
} else {
securityToken = null;
if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR) {
// bad credentials, stop trying to login
stopPolls();
}
}
}
private void getAccount() throws InterruptedException {
HttpResult result = sendRequest(BASE_URL + "/v5/My?expand=account", HttpMethod.GET, securityToken, null, null);
account = parseResultAndUpdateStatus(result, gsonUpperCase, AccountDTO.class);
}
private void getDevices() throws InterruptedException {
AccountDTO localAccount = account;
if (localAccount == null) {
return;
}
HttpResult result = sendRequest(String.format("%s/v5.1/Accounts/%s/Devices", BASE_URL, localAccount.account.id),
HttpMethod.GET, securityToken, null, null);
DevicesDTO devices = parseResultAndUpdateStatus(result, gsonLowerCase, DevicesDTO.class);
if (devices != null) {
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);
}
}
}
});
}
}
private synchronized HttpResult sendRequest(String url, HttpMethod method, @Nullable String token,
@Nullable ContentProvider content, @Nullable String contentType) throws InterruptedException {
try {
Request request = httpClient.newRequest(url).method(method)
.header("MyQApplicationId", "JVM/G9Nwih5BwKgNCjLxiFUQxQijAebyyg8QUHr7JOrP+tuPb8iHfRHKwTmDzHOu")
.header("ApiVersion", "5.1").header("BrandId", "2").header("Culture", "en").agent(userAgent)
.timeout(10, TimeUnit.SECONDS);
if (token != null) {
request = request.header("SecurityToken", token);
}
if (content != null & contentType != null) {
request = request.content(content, contentType);
}
// use asyc jetty as the API service will response with a 401 error when credentials are wrong,
// but not a WWW-Authenticate header which causes Jetty to throw a generic execution exception which
// prevents us from knowing the response code
logger.trace("Sending {} to {}", request.getMethod(), request.getURI());
final CompletableFuture<HttpResult> futureResult = new CompletableFuture<>();
request.send(new BufferingResponseListener() {
@NonNullByDefault({})
@Override
public void onComplete(Result result) {
futureResult.complete(new HttpResult(result.getResponse().getStatus(), getContentAsString()));
}
});
HttpResult result = futureResult.get();
logger.trace("Account Response - status: {} content: {}", result.responseCode, result.content);
return result;
} catch (ExecutionException e) {
return new HttpResult(0, e.getMessage());
}
}
@Nullable
private <T> T parseResultAndUpdateStatus(HttpResult result, Gson parser, Class<T> classOfT) {
if (HttpStatus.isSuccess(result.responseCode)) {
try {
T responseObject = parser.fromJson(result.content, classOfT);
if (responseObject != null) {
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
return responseObject;
}
} catch (JsonSyntaxException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Invalid JSON Response " + result.content);
}
} else if (result.responseCode == HttpStatus.UNAUTHORIZED_401) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Unauthorized - Check Credentials");
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Invalid Response Code " + result.responseCode + " : " + result.content);
}
return null;
}
private class HttpResult {
public final int responseCode;
public @Nullable String content;
public HttpResult(int responseCode, @Nullable String content) {
this.responseCode = responseCode;
this.content = content;
}
}
private static String randomString(int length) {
int low = 97; // a-z
int high = 122; // A-Z
StringBuilder sb = new StringBuilder(length);
Random random = new Random();
for (int i = 0; i < length; i++) {
sb.append((char) (low + (int) (random.nextFloat() * (high - low + 1))));
}
return sb.toString();
}
}

View File

@ -0,0 +1,28 @@
/**
* 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.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.myq.internal.dto.DeviceDTO;
/**
* The {@link MyQDeviceHandler} is responsible for handling device updates
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public interface MyQDeviceHandler {
public void handleDeviceUpdate(DeviceDTO device);
public String getSerialNumber();
}

View File

@ -0,0 +1,131 @@
/**
* 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.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.myq.internal.MyQBindingConstants;
import org.openhab.binding.myq.internal.config.MyQDeviceConfiguration;
import org.openhab.binding.myq.internal.dto.DeviceDTO;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
/**
* The {@link MyQGarageDoorHandler} is responsible for handling commands for a garage door thing, which are
* sent to one of the channels.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class MyQGarageDoorHandler extends BaseThingHandler implements MyQDeviceHandler {
private @Nullable DeviceDTO deviceState;
private String serialNumber;
public MyQGarageDoorHandler(Thing thing) {
super(thing);
serialNumber = getConfigAs(MyQDeviceConfiguration.class).serialNumber;
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
updateState();
return;
}
Bridge bridge = getBridge();
final DeviceDTO localState = deviceState;
if (bridge != null && localState != null) {
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
String cmd = null;
if (command instanceof OnOffType) {
cmd = command == OnOffType.ON ? "open" : "close";
}
if (command instanceof UpDownType) {
cmd = command == UpDownType.UP ? "open" : "close";
}
if (command instanceof PercentType) {
cmd = ((PercentType) command).as(UpDownType.class) == UpDownType.UP ? "open" : "close";
}
if (command instanceof StringType) {
cmd = command.toString();
}
if (cmd != null) {
((MyQAccountHandler) handler).sendAction(localState.serialNumber, cmd);
}
}
}
}
@Override
public String getSerialNumber() {
return serialNumber;
}
protected void updateState() {
final DeviceDTO localState = deviceState;
if (localState != null) {
String doorState = localState.state.doorState;
updateState("status", new StringType(doorState));
switch (doorState) {
case "open":
case "opening":
case "closing":
case "stopped":
case "transition":
updateState("switch", OnOffType.ON);
updateState("rollershutter", UpDownType.UP);
break;
case "closed":
updateState("switch", OnOffType.OFF);
updateState("rollershutter", UpDownType.DOWN);
break;
default:
updateState("switch", UnDefType.UNDEF);
updateState("rollershutter", UnDefType.UNDEF);
break;
}
}
}
@Override
public void handleDeviceUpdate(DeviceDTO device) {
if (!MyQBindingConstants.THING_TYPE_GARAGEDOOR.getId().equals(device.deviceFamily)) {
return;
}
deviceState = device;
if (device.state.online) {
updateStatus(ThingStatus.ONLINE);
updateState();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device reports as offline");
}
}
}

View File

@ -0,0 +1,98 @@
/**
* 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.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.myq.internal.MyQBindingConstants;
import org.openhab.binding.myq.internal.config.MyQDeviceConfiguration;
import org.openhab.binding.myq.internal.dto.DeviceDTO;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link MyQLampHandler} is responsible for handling commands for a lamp thing, which are
* sent to one of the channels.
*
* @author Dan Cunningham - Initial contribution
*/
@NonNullByDefault
public class MyQLampHandler extends BaseThingHandler implements MyQDeviceHandler {
private @Nullable DeviceDTO deviceState;
private String serialNumber;
public MyQLampHandler(Thing thing) {
super(thing);
serialNumber = getConfigAs(MyQDeviceConfiguration.class).serialNumber;
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
updateState();
return;
}
if (command instanceof OnOffType) {
Bridge bridge = getBridge();
final DeviceDTO localState = deviceState;
if (bridge != null && localState != null) {
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
((MyQAccountHandler) handler).sendAction(localState.serialNumber,
command == OnOffType.ON ? "turnon" : "turnoff");
}
}
}
}
@Override
public String getSerialNumber() {
return serialNumber;
}
protected void updateState() {
final DeviceDTO localState = deviceState;
if (localState != null) {
String lampState = localState.state.lampState;
updateState("switch", "on".equals(lampState) ? OnOffType.ON : OnOffType.OFF);
}
}
@Override
public void handleDeviceUpdate(DeviceDTO device) {
if (!MyQBindingConstants.THING_TYPE_LAMP.getId().equals(device.deviceFamily)) {
return;
}
deviceState = device;
if (device.state.online) {
updateStatus(ThingStatus.ONLINE);
updateState();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device reports as offline");
}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="myq" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>MyQ Binding</name>
<description>The MyQ binding allows monitoring and control of garage doors that are MyQ enabled.</description>
</binding:binding>

View File

@ -0,0 +1,37 @@
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:myq:account">
<parameter name="username" type="text" required="true">
<label>User Name</label>
<description>Account username</description>
</parameter>
<parameter name="password" type="text" required="true">
<label>password</label>
<description>Account password</description>
<context>password</context>
</parameter>
<parameter name="refreshInterval" type="integer" min="30" required="true" unit="s">
<label>Refresh Interval</label>
<description>Specifies the refresh interval in seconds</description>
<default>60</default>
</parameter>
</config-description>
<config-description uri="thing-type:myq:garagedoor">
<parameter name="serialNumber" type="text" required="true">
<label>Serial Number</label>
<description>Serial number of the garage door</description>
</parameter>
</config-description>
<config-description uri="thing-type:myq:lamp">
<parameter name="serialNumber" type="text" required="true">
<label>Serial Number</label>
<description>Serial number of the lamp</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="myq" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="account">
<label>MyQ Account</label>
<description>MyQ Cloud Account</description>
<config-description-ref uri="thing-type:myq:account"/>
</bridge-type>
<thing-type id="garagedoor">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>MyQ Garage Door</label>
<description>MyQ Garage Door</description>
<channels>
<channel id="status" typeId="doorstatus"/>
<channel id="switch" typeId="doorswitch"/>
<channel id="rollershutter" typeId="doorrollershutter"/>
</channels>
<representation-property>serialNumber</representation-property>
<config-description-ref uri="thing-type:myq:garagedoor"/>
</thing-type>
<thing-type id="lamp">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>MyQ Lamp</label>
<description>MyQ Lamp</description>
<channels>
<channel id="switch" typeId="lampswitch"/>
</channels>
<representation-property>serialNumber</representation-property>
<config-description-ref uri="thing-type:myq:lamp"/>
</thing-type>
<channel-type id="doorstatus">
<item-type>String</item-type>
<label>Garage Door Status</label>
<state readOnly="true">
<options>
<option value="open">Open</option>
<option value="opening">Opening</option>
<option value="closed">Closed</option>
<option value="closing">Closing</option>
<option value="stopped">Stopped</option>
<option value="transition">Transitioning</option>
<option value="unknown">Unknown</option>
</options>
</state>
</channel-type>
<channel-type id="doorswitch">
<item-type>Switch</item-type>
<label>Garage Door Switch</label>
</channel-type>
<channel-type id="doorrollershutter">
<item-type>Rollershutter</item-type>
<label>Garage Door Rollershutter</label>
</channel-type>
<channel-type id="lampswitch">
<item-type>Switch</item-type>
<label>Lamp Switch</label>
</channel-type>
</thing:thing-descriptions>

View File

@ -202,6 +202,7 @@
<module>org.openhab.binding.mqtt.generic</module>
<module>org.openhab.binding.mqtt.homeassistant</module>
<module>org.openhab.binding.mqtt.homie</module>
<module>org.openhab.binding.myq</module>
<module>org.openhab.binding.mystrom</module>
<module>org.openhab.binding.nanoleaf</module>
<module>org.openhab.binding.neato</module>