From 42edf53a5a9b918c2a45dc5d85fa9258858b167c Mon Sep 17 00:00:00 2001 From: Dan Cunningham Date: Fri, 26 Feb 2021 14:50:25 -0800 Subject: [PATCH] [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 --- CODEOWNERS | 1 + bundles/org.openhab.binding.myq/NOTICE | 13 + bundles/org.openhab.binding.myq/README.md | 68 ++++ bundles/org.openhab.binding.myq/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../myq/internal/MyQBindingConstants.java | 38 ++ .../myq/internal/MyQDiscoveryService.java | 97 +++++ .../myq/internal/MyQHandlerFactory.java | 73 ++++ .../config/MyQAccountConfiguration.java | 27 ++ .../config/MyQDeviceConfiguration.java | 25 ++ .../binding/myq/internal/dto/AccountDTO.java | 38 ++ .../myq/internal/dto/AccountInfoDTO.java | 24 ++ .../binding/myq/internal/dto/ActionDTO.java | 28 ++ .../binding/myq/internal/dto/AddressDTO.java | 26 ++ .../binding/myq/internal/dto/CountryDTO.java | 25 ++ .../binding/myq/internal/dto/DeviceDTO.java | 31 ++ .../myq/internal/dto/DeviceStateDTO.java | 56 +++ .../binding/myq/internal/dto/DevicesDTO.java | 26 ++ .../myq/internal/dto/LoginRequestDTO.java | 30 ++ .../myq/internal/dto/LoginResponseDTO.java | 22 ++ .../binding/myq/internal/dto/TimeZoneDTO.java | 24 ++ .../binding/myq/internal/dto/UsersDTO.java | 23 ++ .../internal/handler/MyQAccountHandler.java | 344 ++++++++++++++++++ .../internal/handler/MyQDeviceHandler.java | 28 ++ .../handler/MyQGarageDoorHandler.java | 131 +++++++ .../myq/internal/handler/MyQLampHandler.java | 98 +++++ .../main/resources/OH-INF/binding/binding.xml | 9 + .../main/resources/OH-INF/config/config.xml | 37 ++ .../resources/OH-INF/thing/thing-types.xml | 67 ++++ bundles/pom.xml | 1 + 30 files changed, 1436 insertions(+) create mode 100644 bundles/org.openhab.binding.myq/NOTICE create mode 100644 bundles/org.openhab.binding.myq/README.md create mode 100644 bundles/org.openhab.binding.myq/pom.xml create mode 100644 bundles/org.openhab.binding.myq/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQBindingConstants.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQDiscoveryService.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQHandlerFactory.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQAccountConfiguration.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQDeviceConfiguration.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountInfoDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/ActionDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AddressDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/CountryDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceStateDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DevicesDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginRequestDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginResponseDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/TimeZoneDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/UsersDTO.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQAccountHandler.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQDeviceHandler.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQGarageDoorHandler.java create mode 100644 bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQLampHandler.java create mode 100644 bundles/org.openhab.binding.myq/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.myq/src/main/resources/OH-INF/config/config.xml create mode 100644 bundles/org.openhab.binding.myq/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index ec1e73f33..82d963dc3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/bundles/org.openhab.binding.myq/NOTICE b/bundles/org.openhab.binding.myq/NOTICE new file mode 100644 index 000000000..38d625e34 --- /dev/null +++ b/bundles/org.openhab.binding.myq/NOTICE @@ -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 index 000000000..a8094c354 --- /dev/null +++ b/bundles/org.openhab.binding.myq/README.md @@ -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 index 000000000..a0e8b2e06 --- /dev/null +++ b/bundles/org.openhab.binding.myq/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.myq + + openHAB Add-ons :: Bundles :: MyQ Binding + + 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 index 000000000..60382373b --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.myq/${project.version} + + 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 index 000000000..3e8cb3057 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQBindingConstants.java @@ -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 SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_GARAGEDOOR, + THING_TYPE_LAMP); + public static final Set 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 index 000000000..883d2988c --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQDiscoveryService.java @@ -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 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 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 index 000000000..2d01eee77 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/MyQHandlerFactory.java @@ -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 index 000000000..b6f3d23c2 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQAccountConfiguration.java @@ -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 index 000000000..8cab68231 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/config/MyQDeviceConfiguration.java @@ -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 index 000000000..295791832 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountDTO.java @@ -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 index 000000000..193f464d0 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AccountInfoDTO.java @@ -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 index 000000000..a38aa1e07 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/ActionDTO.java @@ -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 index 000000000..1c4d858ff --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/AddressDTO.java @@ -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 index 000000000..594a1ee32 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/CountryDTO.java @@ -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 index 000000000..426d691b2 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceDTO.java @@ -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 index 000000000..169119b8c --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DeviceStateDTO.java @@ -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 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 index 000000000..f170101c8 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/DevicesDTO.java @@ -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 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 index 000000000..8b2eaf540 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginRequestDTO.java @@ -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 index 000000000..2dfcd637d --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/LoginResponseDTO.java @@ -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 index 000000000..e324225ec --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/TimeZoneDTO.java @@ -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 index 000000000..c988dba88 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/dto/UsersDTO.java @@ -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 index 000000000..f9dc5f11b --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQAccountHandler.java @@ -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> 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 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 parseResultAndUpdateStatus(HttpResult result, Gson parser, Class 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 index 000000000..030d780be --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQDeviceHandler.java @@ -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 index 000000000..c83c4acda --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQGarageDoorHandler.java @@ -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 index 000000000..e7988bae0 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/java/org/openhab/binding/myq/internal/handler/MyQLampHandler.java @@ -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 index 000000000..b024ec745 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + MyQ Binding + The MyQ binding allows monitoring and control of garage doors that are MyQ enabled. + + 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 index 000000000..beaf772f2 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,37 @@ + + + + + + Account username + + + + Account password + password + + + + Specifies the refresh interval in seconds + 60 + + + + + + + Serial number of the garage door + + + + + + + Serial number of the lamp + + + + 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 index 000000000..dbe689468 --- /dev/null +++ b/bundles/org.openhab.binding.myq/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,67 @@ + + + + + + MyQ Cloud Account + + + + + + + + + MyQ Garage Door + + + + + + serialNumber + + + + + + + + + MyQ Lamp + + + + serialNumber + + + + + String + + + + + + + + + + + + + + + Switch + + + + Rollershutter + + + + Switch + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 909b8d78d..4f38c9a01 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -202,6 +202,7 @@ org.openhab.binding.mqtt.generic org.openhab.binding.mqtt.homeassistant org.openhab.binding.mqtt.homie + org.openhab.binding.myq org.openhab.binding.mystrom org.openhab.binding.nanoleaf org.openhab.binding.neato