]> git.basschouten.com Git - openhab-addons.git/commitdiff
[MyQ] Initial commit of the MyQ binding for OH3 (#9347)
authorDan Cunningham <dan@digitaldan.com>
Fri, 26 Feb 2021 22:50:25 +0000 (14:50 -0800)
committerGitHub <noreply@github.com>
Fri, 26 Feb 2021 22:50:25 +0000 (14:50 -0800)
* 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>
30 files changed:
CODEOWNERS
bundles/org.openhab.binding.myq/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.myq/README.md [new file with mode: 0644]
bundles/org.openhab.binding.myq/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQAccountConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQDeviceConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountInfoDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/ActionDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AddressDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/CountryDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceStateDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DevicesDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginRequestDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginResponseDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/TimeZoneDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/UsersDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQAccountHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQDeviceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQGarageDoorHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQLampHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.myq/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index ec1e73f33f4e17a85f5de17503b7153d6576bb6a..82d963dc34f51164d646dd84af74f469a3960533 100644 (file)
 /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
diff --git a/bundles/org.openhab.binding.myq/NOTICE b/bundles/org.openhab.binding.myq/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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
diff --git a/bundles/org.openhab.binding.myq/README.md b/bundles/org.openhab.binding.myq/README.md
new file mode 100644 (file)
index 0000000..a8094c3
--- /dev/null
@@ -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
+  }                
+}
+```
diff --git a/bundles/org.openhab.binding.myq/pom.xml b/bundles/org.openhab.binding.myq/pom.xml
new file mode 100644 (file)
index 0000000..a0e8b2e
--- /dev/null
@@ -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>
diff --git a/bundles/org.openhab.binding.myq/src/main/feature/feature.xml b/bundles/org.openhab.binding.myq/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..6038237
--- /dev/null
@@ -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>
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQBindingConstants.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQBindingConstants.java
new file mode 100644 (file)
index 0000000..3e8cb30
--- /dev/null
@@ -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);
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQDiscoveryService.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQDiscoveryService.java
new file mode 100644 (file)
index 0000000..883d298
--- /dev/null
@@ -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();
+    }
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQHandlerFactory.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQHandlerFactory.java
new file mode 100644 (file)
index 0000000..2d01eee
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQAccountConfiguration.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQAccountConfiguration.java
new file mode 100644 (file)
index 0000000..b6f3d23
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQDeviceConfiguration.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQDeviceConfiguration.java
new file mode 100644 (file)
index 0000000..8cab682
--- /dev/null
@@ -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 = "";
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountDTO.java
new file mode 100644 (file)
index 0000000..2957918
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountInfoDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountInfoDTO.java
new file mode 100644 (file)
index 0000000..193f464
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/ActionDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/ActionDTO.java
new file mode 100644 (file)
index 0000000..a38aa1e
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AddressDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AddressDTO.java
new file mode 100644 (file)
index 0000000..1c4d858
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/CountryDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/CountryDTO.java
new file mode 100644 (file)
index 0000000..594a1ee
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceDTO.java
new file mode 100644 (file)
index 0000000..426d691
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceStateDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceStateDTO.java
new file mode 100644 (file)
index 0000000..169119b
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DevicesDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DevicesDTO.java
new file mode 100644 (file)
index 0000000..f170101
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginRequestDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginRequestDTO.java
new file mode 100644 (file)
index 0000000..8b2eaf5
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginResponseDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginResponseDTO.java
new file mode 100644 (file)
index 0000000..2dfcd63
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/TimeZoneDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/TimeZoneDTO.java
new file mode 100644 (file)
index 0000000..e324225
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/UsersDTO.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/UsersDTO.java
new file mode 100644 (file)
index 0000000..c988dba
--- /dev/null
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQAccountHandler.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQAccountHandler.java
new file mode 100644 (file)
index 0000000..f9dc5f1
--- /dev/null
@@ -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();
+    }
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQDeviceHandler.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQDeviceHandler.java
new file mode 100644 (file)
index 0000000..030d780
--- /dev/null
@@ -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();
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQGarageDoorHandler.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQGarageDoorHandler.java
new file mode 100644 (file)
index 0000000..c83c4ac
--- /dev/null
@@ -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");
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQLampHandler.java b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQLampHandler.java
new file mode 100644 (file)
index 0000000..e7988ba
--- /dev/null
@@ -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");
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..b024ec7
--- /dev/null
@@ -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>
diff --git a/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..beaf772
--- /dev/null
@@ -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>
diff --git a/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..dbe6894
--- /dev/null
@@ -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>
index 909b8d78d80eba2dba7de1a9c93f9ca5caf009ab..4f38c9a0194214ec52d3e09c34471852b8abdbe1 100644 (file)
     <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>