diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 23feb8be9..11519954b 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -171,6 +171,11 @@ org.openhab.binding.cbus ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.chamberlainmyq + ${project.version} + org.openhab.addons.bundles org.openhab.binding.chromecast diff --git a/bundles/org.openhab.binding.chamberlainmyq/NOTICE b/bundles/org.openhab.binding.chamberlainmyq/NOTICE new file mode 100644 index 000000000..4c20ef446 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/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/openhab2-addons diff --git a/bundles/org.openhab.binding.chamberlainmyq/README.md b/bundles/org.openhab.binding.chamberlainmyq/README.md new file mode 100644 index 000000000..829e31207 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/README.md @@ -0,0 +1,90 @@ +# ChamberlainMyQ Binding + +[Chamberlain MyQ](http://www.chamberlain.com/smartphone-control-products/myq-smartphone-control) system allows you to connect your garage door to the internet to be controlled from anywhere using your smartphone. Using this API, The Chamberlain MyQ Binding can get the status of your garage door opener and send commands to open or close it. + +## Supported Things + +``` +MyQ Gateway Thing Bridge - Gateway to MyQ Online API, Must be configured to use binding. +MyQ Garage Door Thing - Standard Chamberlain Garage Door. +MyQ Light Switch Thing - Chamberlain MyQ Controlled Light Switch or Plugin Module. +``` + +## Discovery + +Gararge Doors and Light Modules Things will be auto Discovery after a MYQ Gateway Thing is added manually. + + +## Thing Configuration + +The Chamberlain MyQ Gateway Thing only requires your Chamberlain MyQ Username and Password. + +| Property | Default | Required | Description | +|--------------|---------|:--------:|-------------| +| username | | Yes | Chamberlain MyQ Username | +| password | | Yes | Chamberlain MyQ Password | +| refresh | 60 | No | Data refresh interval in seconds | +| quickrefresh | 2 | No | Data refresh interval after Event is trigger in seconds | +| timeout | 25 | No | Timeout for HTTP requests in seconds | + +## Channels +``` +Door State - Garage Door Open/Close State. +Roller State - Garage Door Roller Shutter State. +Door Status - Garage Door Status as a string. +Door Open - Garage Door Is Open Contact. +Door Closed - Garage Door Is Closed Contact. +Light State - Light On/Off State. +Serial Number - MyQ Device Serial Number. +Description - MyQ Device Description, Should Match Device User specified Name in the MyQ app. +``` + +## Examples + +### Items + +``` +Switch LampModule "Lamp Module On" {channel="chamberlainmyq:MyQLight:344bf0bc:108573427:lightstate"} +String LampModuleDesc "Lamp Module Desc [%s]" {channel="chamberlainmyq:MyQLight:344bf0bc:108573427:name"} +String LampModuleSerialNumber "Lamp Module SerialNumber [%s]" {channel="chamberlainmyq:MyQLight:344bf0bc:108573427:serialnumber"} + +Switch LightSwitch "Light Switch On" {channel="chamberlainmyq:MyQLight:344bf0bc:20384288:lightstate"} +String LightSwitchDesc "Light Switch Desc [%s]" {channel="chamberlainmyq:MyQLight:344bf0bc:20384288:name"} +String LightSwitchSerialNumber "Light Switch SerialNumber [%s]" {channel="chamberlainmyq:MyQLight:344bf0bc:20384288:serialnumber"} + +Switch GarageDoorSwitch "Garage Door Open" {channel="chamberlainmyq:MyQDoorOpener:344bf0bc:1810905:doorstate"} +String GarageDoorString "Garage Door [%s]" {channel="chamberlainmyq:MyQDoorOpener:344bf0bc:1810905:doorstatus"} +Rollershutter GarageDoorShutter "Garage Door Open" {channel="chamberlainmyq:MyQDoorOpener:344bf0bc:1810905:rollerstate"} +Contact GarageDoorOpenContact "Garage Door Open [%s]" {channel="chamberlainmyq:MyQDoorOpener:344bf0bc:1810905:dooropen"} +Contact GarageDoorClosedContact "Garage Door Closed [%s]" {channel="chamberlainmyq:MyQDoorOpener:344bf0bc:1810905:doorclosed"} +String GarageDoorDesc "Garage Door Desc [%s]" {channel="chamberlainmyq:MyQDoorOpener:344bf0bc:1810905:name"} +String GarageDoorSerialNumber "Garage Door SerialNumber [%s]" {channel="chamberlainmyq:MyQDoorOpener:344bf0bc:1810905:serialnumber"} +``` + +### Sitemap + +``` +Switch item=LampModule +Text item=LampModuleDesc +Text item=LampModuleSerialNumber + +Switch item=LightSwitch +Text item=LightSwitchDesc +Text item=LightSwitchSerialNumber + +Switch item=GarageDoorSwitch +Text item=GarageDoorString +Switch item=GarageDoorShutter +Text item=GarageDoorOpenContact +Text item=GarageDoorClosedContact +Text item=GarageDoorDesc +Text item=GarageDoorSerialNumber +``` + +## Known Working Hardware + +| Model | Name | +|-----------|------| +| HD950WF | Chamberlain 1.25 hps Wi-Fi Garage Door Opener | +| 825LM | Liftmaster 825LM Remote Light Module | +| 823LM | Liftmaster 823LM Remote Light Switch | \ No newline at end of file diff --git a/bundles/org.openhab.binding.chamberlainmyq/pom.xml b/bundles/org.openhab.binding.chamberlainmyq/pom.xml new file mode 100644 index 000000000..df87edea2 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/pom.xml @@ -0,0 +1,36 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.0.0-SNAPSHOT + + + org.openhab.binding.chamberlainmyq + + openHAB Add-ons :: Bundles :: ChamberlainMyQ Binding + + + org.apache.http*;resolution:=optional,org.apache.xerces.impl*;resolution:=optional + + + + + commons-collections + commons-collections + 3.2.2 + compile + + + commons-io + commons-io + 2.5 + compile + + + + diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/feature/feature.xml b/bundles/org.openhab.binding.chamberlainmyq/src/main/feature/feature.xml new file mode 100644 index 000000000..c855d6339 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab.tp-jackson + mvn:org.openhab.addons.bundles/org.openhab.binding.chamberlainmyq/${project.version} + + diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/ChamberlainMyQBindingConstants.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/ChamberlainMyQBindingConstants.java new file mode 100644 index 000000000..6084ef2b0 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/ChamberlainMyQBindingConstants.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link ChamberlainMyQBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Scott - Initial contribution + */ +@NonNullByDefault +public class ChamberlainMyQBindingConstants { + + public static final String BINDING_ID = "chamberlainmyq"; + + // bridge + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "MyQGateway"); + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_DOOR_OPENER = new ThingTypeUID(BINDING_ID, "MyQDoorOpener"); + public static final ThingTypeUID THING_TYPE_LIGHT = new ThingTypeUID(BINDING_ID, "MyQLight"); + + // List of all Channel ids + public static final String CHANNEL_LIGHT_STATE = "lightstate"; + public static final String CHANNEL_DOOR_STATE = "doorstate"; + public static final String CHANNEL_ROLLER_STATE = "rollerstate"; + public static final String CHANNEL_DOOR_STATUS = "doorstatus"; + public static final String CHANNEL_DOOR_OPEN = "dooropen"; + public static final String CHANNEL_DOOR_CLOSED = "doorclosed"; + + public static final String CHANNEL_TYPE = "devicetype"; + public static final String CHANNEL_NAME = "devicename"; + public static final String CHANNEL_SERIAL_NUMBER = "serialnumber"; + + // Bridge config properties + public static final String USER_NAME = "username"; + public static final String PASSWORD = "password"; + public static final String POLL_PERIOD = "pollPeriod"; + public static final String QUICK_POLL_PERIOD = "quickPollPeriod"; + public static final String TIME_OUT = "timeout"; + + // Door Opener/Light config properties + public static final String MYQ_TYPE = "device_type"; + public static final String MYQ_FAMILY = "device_family"; + public static final String MYQ_SERIAL = "serial_number"; + public static final String MYQ_NAME = "name"; + public static final String MYQ_ONLINE = "online"; + public static final String MYQ_DOORSTATE = "door_state"; + public static final String MYQ_LAMPSTATE = "lamp_state"; + public static final String MYQ_STATE = "state"; + + // API Information + public static final String WEBSITE = "https://api.myqdevice.com"; + public static final String APP_ID = "JVM/G9Nwih5BwKgNCjLxiFUQxQijAebyyg8QUHr7JOrP+tuPb8iHfRHKwTmDzHOu"; + public static final String CULTURE = "en"; + public static final String BRANDID = "2"; + public static final String USERAGENT = "Chamberlain/3773 (iPhone; iOS 11.0.3; Scale/2.00)"; + public static final String APIVERSION = "4.1"; +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/config/ChamberlainMyQDeviceConfig.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/config/ChamberlainMyQDeviceConfig.java new file mode 100644 index 000000000..8f61236f4 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/config/ChamberlainMyQDeviceConfig.java @@ -0,0 +1,203 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.config; + +import static org.openhab.binding.chamberlainmyq.ChamberlainMyQBindingConstants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.ThingStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonObject; + +/** + * The {@link ChamberlainMyQDeviceConfig} class represents the common configuration + * parameters of a MyQ Device + * + * @author Scott Hanson - Initial contribution + * + */ +public class ChamberlainMyQDeviceConfig { + private String deviceType; + private String deviceFamily; + private String state; + private String name; + private String serialNumber; + private boolean online; + + private Logger logger = LoggerFactory.getLogger(ChamberlainMyQDeviceConfig.class); + + public ChamberlainMyQDeviceConfig(Map properties) { + this.deviceType = properties.get(MYQ_TYPE).toString().replaceAll("\"", ""); + this.state = properties.get(MYQ_STATE).toString().replaceAll("\"", ""); + this.name = properties.get(MYQ_NAME).toString().replaceAll("\"", ""); + this.serialNumber = properties.get(MYQ_SERIAL).toString().replaceAll("\"", ""); + this.deviceFamily = properties.get(MYQ_FAMILY).toString().replaceAll("\"", ""); + + this.online = Boolean.valueOf(properties.get(MYQ_ONLINE).toString()); + } + + public ChamberlainMyQDeviceConfig(JsonObject jsonConfig) { + this.serialNumber = jsonConfig.get(MYQ_SERIAL).toString().replaceAll("\"", ""); + readConfigFromJson(jsonConfig); + } + + public void readConfigFromJson(JsonObject jsonConfig) { + this.deviceType = jsonConfig.get(MYQ_TYPE).toString().replaceAll("\"", ""); + this.serialNumber = jsonConfig.get(MYQ_SERIAL).toString().replaceAll("\"", ""); + this.name = jsonConfig.get(MYQ_NAME).toString().toString().replaceAll("\"", ""); + this.deviceFamily = jsonConfig.get(MYQ_FAMILY).toString().replaceAll("\"", ""); + + logger.trace("readConfigFromJson, reading states"); + + JsonObject jsonState = jsonConfig.get(MYQ_STATE).getAsJsonObject(); + + if (jsonState != null) { + logger.trace("jsonState {}", jsonState); + } else { + logger.trace("jsonState is null"); + } + if (jsonState != null && jsonState.has(MYQ_DOORSTATE)) { + logger.trace("has {}", MYQ_DOORSTATE); + this.state = jsonState.get(MYQ_DOORSTATE).getAsString(); + } + if (jsonState != null && jsonState.has(MYQ_LAMPSTATE)) { + logger.trace("has {}", MYQ_LAMPSTATE); + this.state = jsonState.get(MYQ_LAMPSTATE).getAsString(); + } + if (jsonState != null && jsonState.has(MYQ_ONLINE)) { + logger.trace("has {}", MYQ_ONLINE); + this.online = Boolean.valueOf(jsonState.get(MYQ_ONLINE).toString()); + } + } + + public String asString() { + return ("Device Serial: " + serialNumber + "\n" + "Device Type: " + deviceType + "\n" + "Device Family: " + + deviceFamily + "\n" + "\n" + "Name: " + name + "\n" + "State: " + state + "\n" + "Online: " + + online + "\n"); + } + + public boolean validateConfig() { + if (this.serialNumber == null) { + return false; + } + return true; + } + + public String getDeviceStatus() { + return state; + } + + public OnOffType getDoorStatusOnOff() { + if (isDoorClosed()) { + return OnOffType.OFF; + } else { + return OnOffType.ON; + } + } + + public OnOffType getLightStatusOnOff() { + if (state.compareTo("on") == 0) { + return OnOffType.ON; + } else { + return OnOffType.OFF; + } + } + + public PercentType getDeviceStatusPercent() { + if (isDoorOpen()) { + return PercentType.ZERO; + } + if (isDoorClosed()) { + return PercentType.HUNDRED; + } + return new PercentType(50); + } + + public OpenClosedType isDoorClosedContact() { + if (isDoorClosed()) { + return OpenClosedType.CLOSED; + } + return OpenClosedType.OPEN; + } + + public OpenClosedType isDoorOpenContact() { + if (isDoorOpen()) { + return OpenClosedType.CLOSED; + } + return OpenClosedType.OPEN; + } + + public boolean isDoorClosed() { + if (state.compareTo("closed") == 0) { + return true; + } + return false; + } + + public boolean isDoorOpen() { + if (state.compareTo("open") == 0) { + return true; + } + return false; + } + + public String getSerialNumber() { + return serialNumber; + } + + public String getName() { + return name; + } + + public String getDeviceType() { + return deviceType; + } + + public String getDeviceFamily() { + return deviceFamily; + } + + public String getState() { + return state; + } + + public boolean getOnline() { + return online; + } + + public ThingStatus getThingOnline() { + if (online) { + return ThingStatus.ONLINE; + } else { + return ThingStatus.OFFLINE; + } + } + + public Map getProperties() { + Map properties = new HashMap<>(); + properties.put(MYQ_TYPE, getDeviceType()); + properties.put(MYQ_SERIAL, getSerialNumber()); + properties.put(MYQ_STATE, getState()); + properties.put(MYQ_NAME, getName()); + properties.put(MYQ_ONLINE, getOnline()); + properties.put(MYQ_FAMILY, getDeviceFamily()); + return properties; + } +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/config/ChamberlainMyQGatewayConfig.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/config/ChamberlainMyQGatewayConfig.java new file mode 100644 index 000000000..e0583e0f7 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/config/ChamberlainMyQGatewayConfig.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.config; + +import static org.openhab.binding.chamberlainmyq.ChamberlainMyQBindingConstants.*; + +/** + * The {@link ChamberlainMyQGatewayConfig} class represents the configuration of a MyQ Gateway Interface + * + * @author Scott Hanson - Initial contribution + * + */ +public class ChamberlainMyQGatewayConfig { + public String username; + public String password; + public int timeout; + public int pollPeriod; + public int quickPollPeriod; + public String appID = APP_ID; + public String brandID = BRANDID; +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQDoorOpenerHandler.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQDoorOpenerHandler.java new file mode 100644 index 000000000..8363925d1 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQDoorOpenerHandler.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.handler; + +import static org.openhab.binding.chamberlainmyq.ChamberlainMyQBindingConstants.*; + +import java.io.IOException; + +import org.openhab.binding.chamberlainmyq.internal.InvalidLoginException; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.types.UpDownType; +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.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonObject; + +/** + * The {@link ChamberlainMyQDoorOpenerHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Scott Hanson - Initial contribution + */ + +public class ChamberlainMyQDoorOpenerHandler extends ChamberlainMyQHandler { + + private Logger logger = LoggerFactory.getLogger(ChamberlainMyQDoorOpenerHandler.class); + + public ChamberlainMyQDoorOpenerHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + if (!this.deviceConfig.validateConfig()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid config."); + return; + } + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void channelLinked(ChannelUID channelUID) { + if (channelUID.getId().equals(CHANNEL_DOOR_STATE) || channelUID.getId().equals(CHANNEL_DOOR_STATUS) + || channelUID.getId().equals(CHANNEL_ROLLER_STATE) || channelUID.getId().equals(CHANNEL_SERIAL_NUMBER) + || channelUID.getId().equals(CHANNEL_NAME) || channelUID.getId().equals(CHANNEL_DOOR_OPEN) + || channelUID.getId().equals(CHANNEL_DOOR_CLOSED) || channelUID.getId().equals(CHANNEL_TYPE)) { + readDeviceState(); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (channelUID.getId().equals(CHANNEL_DOOR_STATE) || channelUID.getId().equals(CHANNEL_ROLLER_STATE)) { + if (command.equals(OnOffType.ON) || command.equals(UpDownType.UP)) { + setDoorState(true); + } else if (command.equals(OnOffType.OFF) || command.equals(UpDownType.DOWN)) { + setDoorState(false); + } else if (command instanceof RefreshType) { + readDeviceState(); + } + } + } + + private void setDoorState(boolean state) { + String deviceSerial = getThing().getProperties().get(MYQ_SERIAL); + try { + if (state) { + getGatewayHandler().executeMyQCommand(deviceSerial, "open", true); + } else { + getGatewayHandler().executeMyQCommand(deviceSerial, "close", true); + } + } catch (InvalidLoginException e) { + logger.error("Error Setting Door State: {}", e.getMessage()); + } catch (IOException e) { + logger.error("Error Setting Door State: {}", e.getMessage()); + } + } + + @Override + public void updateState(JsonObject jsonDataBlob) { + deviceConfig.readConfigFromJson(jsonDataBlob); + logger.trace("updateState: {}", deviceConfig.getSerialNumber()); + updateState(CHANNEL_DOOR_STATE, deviceConfig.getDoorStatusOnOff()); + updateState(CHANNEL_DOOR_STATUS, new StringType(deviceConfig.getDeviceStatus())); + updateState(CHANNEL_ROLLER_STATE, deviceConfig.getDeviceStatusPercent()); + updateState(CHANNEL_NAME, StringType.valueOf(deviceConfig.getName())); + updateState(CHANNEL_TYPE, StringType.valueOf(deviceConfig.getDeviceType())); + updateState(CHANNEL_SERIAL_NUMBER, StringType.valueOf(deviceConfig.getSerialNumber())); + updateState(CHANNEL_DOOR_OPEN, deviceConfig.isDoorOpenContact()); + updateState(CHANNEL_DOOR_CLOSED, deviceConfig.isDoorClosedContact()); + updateStatus(deviceConfig.getThingOnline()); + } +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQGatewayHandler.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQGatewayHandler.java new file mode 100644 index 000000000..d29536bc6 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQGatewayHandler.java @@ -0,0 +1,544 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.handler; + +import static org.openhab.binding.chamberlainmyq.ChamberlainMyQBindingConstants.*; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.openhab.binding.chamberlainmyq.config.ChamberlainMyQGatewayConfig; +import org.openhab.binding.chamberlainmyq.internal.ChamberlainMyQResponseCode; +import org.openhab.binding.chamberlainmyq.internal.HttpUtil; +import org.openhab.binding.chamberlainmyq.internal.InvalidLoginException; +import org.openhab.binding.chamberlainmyq.internal.discovery.ChamberlainMyQDeviceDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryService; +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.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +//import org.openhab.core.io.net.http.HttpUtil; + +/** + * The {@link ChamberlainMyQGatewayHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Scott Hanson - Initial contribution + */ +public class ChamberlainMyQGatewayHandler extends BaseBridgeHandler { + + private Logger logger = LoggerFactory.getLogger(ChamberlainMyQGatewayHandler.class); + private final BundleContext bundleContext = FrameworkUtil.getBundle(DiscoveryService.class).getBundleContext(); + private String securityToken; + private String accountId; + ScheduledFuture mainPollRefreshJob; + private int refreshInterval; + private int quickPoll; + + private ScheduledExecutorService pollService = Executors.newSingleThreadScheduledExecutor(); + + /** + * The regular polling task + */ + private ScheduledFuture pollFuture; + + /** + * This task will reset the poll interval back to normal after a rapid poll + * cycle + */ + private ScheduledFuture pollResetFuture; + + /** + * Cap the time we poll rapidly to not overwhelm the servers with api + * requests. + */ + private static int MAX_RAPID_REFRESH = 30 * 1000; + // private Properties header; + + public ChamberlainMyQGatewayHandler(Bridge bridge) { + super(bridge); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.error("The gateway doesn't support any command!"); + } + + @Override + public void initialize() { + this.config = getThing().getConfiguration().as(ChamberlainMyQGatewayConfig.class); + if (validConfiguration()) { + ChamberlainMyQDeviceDiscoveryService discovery = new ChamberlainMyQDeviceDiscoveryService(this); + + this.bundleContext.registerService(DiscoveryService.class, discovery, null); + + refreshInterval = config.pollPeriod; + quickPoll = config.quickPollPeriod; + startAutomaticRefresh(); + updateStatus(ThingStatus.ONLINE); + } + } + + @Override + public void dispose() { + if (mainPollRefreshJob != null) { + mainPollRefreshJob.cancel(true); + } + if (pollFuture != null) { + pollFuture.cancel(true); + } + if (pollResetFuture != null) { + pollResetFuture.cancel(true); + } + super.dispose(); + } + + private boolean validConfiguration() { + if (this.config == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Gateway configuration missing"); + return false; + } else if (StringUtils.isEmpty(this.config.username)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "username not specified"); + return false; + } else if (StringUtils.isEmpty(this.config.password)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "password not specified"); + return false; + } else if (!login()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "failed to login to Chamberlain MyQ Service"); + return false; + } + return true; + } + + public ChamberlainMyQGatewayConfig getGatewayConfig() { + return this.config; + } + + private ChamberlainMyQGatewayConfig config; + + // REST API variables + /** + * Returns the currently cached security token, this will make a call to + * login if the token does not exist. + * + * @return The cached security token + * @throws IOException + * @throws InvalidLoginException + */ + private String getSecurityToken() throws IOException, InvalidLoginException { + if (securityToken == null) { + login(); + } + return securityToken; + } + + /** + * Returns the currently cached Account ID, this will make a call to + * findAccount if the Account ID is not known. + * + * @return The cached Account ID + * @throws IOException + * @throws InvalidLoginException + */ + private String getAccountID() throws IOException, InvalidLoginException { + + if (accountId == null) { + findAccount(); + } + return accountId; + } + + /** + * Get the Account ID for the current user + */ + private boolean findAccount() throws InvalidLoginException, IOException { + if (securityToken == null) { + login(); + } + logger.trace("attempting to get acount"); + String url = String.format("%s/api/v5/My/?expand=account", WEBSITE); + + String message = "{\"expand\":\"account\"}"; + // header.put("SecurityToken", getSecurityToken()); + JsonObject data = request("GET", url, null, null, true, false, enc(getSecurityToken()), false); + + if (data.isJsonNull()) { + logger.error("getting myq accountId failed"); + return false; + } + + JsonElement JsonAccount = data.get("Account"); + if (JsonAccount.isJsonObject()) { + if (!JsonAccount.getAsJsonObject().get("Id").isJsonNull()) { + accountId = JsonAccount.getAsJsonObject().get("Id").getAsString(); + logger.debug("myq AccountId: {}", accountId); + return true; + } + } + + logger.error("getting myq accountId failed"); + return false; + } + + private boolean login() { + logger.debug("attempting to login"); + + String url = String.format("%s/api/v5/Login", WEBSITE); + + String message = String.format("{\"Username\":\"%s\",\"Password\":\"%s\"}", this.config.username, + this.config.password); + // Result result = http.post(url, message); + // logger.debug(result.getBody()); + JsonObject data = request("POST", url, message, "application/json", true, true, "", false); + + if (data.isJsonNull()) { + logger.error("getting myq securityToken failed"); + return false; + } + + if (!data.get("SecurityToken").isJsonNull()) { + securityToken = data.get("SecurityToken").getAsString(); + logger.debug("myq securityToken: {}", securityToken); + return true; + } + logger.error("getting myq securityToken failed"); + return false; + } + + /** + * Retrieves MyQ device data from myq website, throws if connection + * fails or user login fails + * + */ + public JsonObject getMyqData() throws InvalidLoginException, IOException { + logger.debug("Retrieving door data"); + String tempToken = getSecurityToken(); + String url = String.format("%s/api/v5.1/Accounts/%s/Devices", WEBSITE, getAccountID()); + + JsonObject data = request("GET", url, null, null, true, false, enc(tempToken), false); + + return data; + } + + /** + * Send Command to open/close garage door opener with MyQ API Returns false + * if return code from API is not correct or connection fails + * + * @param deviceSerial MyQ deviceSerial of Garage Door Opener. + * @param command command value "open", "close", "turnon" , or "turnoff" + * @param state Desired state to put the door in, 1 = open, 0 = closed + * Desired state to put the lamp in, 1 = on, 0 = off + */ + public void executeMyQCommand(String deviceSerial, String command, boolean rapidPoll) + throws InvalidLoginException, IOException { + String tempToken = getSecurityToken(); + String message = String.format("{\"action_type\":\"%s\"}", command); + String url = String.format("%s/api/v5.1/Accounts/%s/Devices/%s/actions", WEBSITE, getAccountID(), deviceSerial); + + request("PUT", url, message, "application/json", true, false, enc(tempToken), true); + + if (rapidPoll) { + beginRapidPoll(); + } else { + doFuturePoll(quickPoll * 1000); + } + } + + public interface RequestCallback { + public void parseRequestResult(JsonObject resultJson); + + public void onError(String error); + } + + protected class Request implements Runnable { + private RequestCallback callback; + + public Request(RequestCallback callback) { + this.callback = callback; + } + + protected String checkForFailure(JsonObject jsonResult) { + if (jsonResult.get("data").isJsonNull()) { + return jsonResult.get("errors").getAsString(); + } + return null; + } + + @Override + public void run() { + try { + JsonObject resultJson = getMyqData(); + callback.parseRequestResult(resultJson); + } catch (Exception e) { + logger.error("An exception occurred while executing a request to the Gateway: '{}'", e.getMessage()); + } + } + } + + public void sendRequestToServer(RequestCallback callback) throws IOException { + Request request = new Request(callback); + request.run(); + } + + private void startAutomaticRefresh() { + mainPollRefreshJob = scheduler.scheduleAtFixedRate(() -> { + try { + refreshDeviceState(); + } catch (Exception e) { + logger.debug("Exception occurred during refresh: {}", e.getMessage(), e); + } + }, 0, refreshInterval, TimeUnit.SECONDS); + } + + private synchronized void refreshDeviceState() { + try { + logger.trace("refreshDeviceState"); + JsonObject resultJson = getMyqData(); + Bridge bridge = getThing(); + + List things = bridge.getThings(); + if (!resultJson.get("items").isJsonNull()) { + JsonElement deviceData = resultJson.get("items"); + Iterator deviceDataIter = deviceData.getAsJsonArray().iterator(); + while (deviceDataIter.hasNext()) { + JsonElement element = deviceDataIter.next(); + if (!element.isJsonObject()) { + continue; + } + if (element.getAsJsonObject().get(MYQ_SERIAL) != null) { + String findDeviceSerial = element.getAsJsonObject().get(MYQ_SERIAL).getAsString(); + for (Thing thing : things) { + if (thing.getUID().getId().compareTo(findDeviceSerial) == 0) { + ChamberlainMyQHandler test = (ChamberlainMyQHandler) thing.getHandler(); + if (test != null) { + logger.trace("updating state"); + test.updateState(element.getAsJsonObject()); + } + } + } + } + + } + } + } catch (Exception e) { + logger.error("An exception occurred while executing a request to the Gateway: '{}'", e.getMessage()); + } + } + + // UTF-8 URL encode + private String enc(String str) { + try { + return URLEncoder.encode(str, "utf-8"); + } catch (UnsupportedEncodingException e) { + // throw new EndOfTheWorldException() + throw new UnsupportedOperationException("UTF-8 not supported"); + } + } + + /** + * Make a request to the server, optionally retry the call if there is a + * login issue. Will throw a InvalidLoginExcpetion if the account is + * invalid, locked or soon to be locked. + * + * @param method The Http Method Type (GET,PUT) + * @param url The request URL + * @param payload Payload string for put operations + * @param payloadType Payload content type for put operations + * @param retry Retry the attempt if our session key is not valid + * @return The JsonNode representing the response data + * @throws IOException + * @throws InvalidLoginException + */ + private synchronized JsonObject request(String method, String url, String payload, String payloadType, + boolean retry, boolean login, String securityToken, boolean command) { + Properties header; + header = new Properties(); + header.put("Accept", "application/json"); + // header.put("Connection", "keep-alive"); + header.put("Content-Type", "application/json"); + // header.put("User-Agent", USERAGENT); + // logger.debug("User-Agent: {}", USERAGENT); + header.put("BrandId", BRANDID); + logger.trace("BrandId: {}", BRANDID); + // header.put("ApiVersion", APIVERSION); + // logger.debug("ApiVersion: {}", APIVERSION); + // header.put("Culture", CULTURE); + // logger.debug("Culture: {}", CULTURE); + if (!login) { + header.put("SecurityToken", securityToken); + logger.trace("SecurityToken: {}", securityToken); + } + header.put("MyQApplicationId", APP_ID); + logger.trace("MyQApplicationId: {}", APP_ID); + logger.trace("Requesting method {}", method); + logger.trace("Requesting URL {}", url); + logger.trace("Requesting payload {}", payload); + logger.trace("Requesting payloadType {}", payloadType); + + String dataString; + try { + dataString = HttpUtil.executeUrl(method, url, header, + payload == null ? null : IOUtils.toInputStream(payload), payloadType, (this.config.timeout * 1000), + !command); + + if (command && dataString == null) + return null; + + logger.trace("Received MyQ JSON: {}", dataString); + + if (dataString == null) { + logger.error("Null response from MyQ server"); + throw new IOException("Null response from MyQ server"); + } + } catch (Exception e) { + logger.error("Requesting URL Failed", e); + return new JsonObject(); + } + try { + JsonParser parser = new JsonParser(); + JsonObject rootNode = parser.parse(dataString).getAsJsonObject(); + logger.trace("myq parser.parse worked"); + if (!rootNode.has("code")) { + logger.trace("myq request: no code found, Success!"); + return rootNode; + } + int returnCode = Integer.parseInt(rootNode.get("code").getAsString()); + logger.trace("myq ReturnCode: {}", returnCode); + + ChamberlainMyQResponseCode rc = ChamberlainMyQResponseCode.fromCode(returnCode); + + switch (rc) { + case OK: { + return rootNode; + } + case ACCOUNT_INVALID: + case ACCOUNT_NOT_FOUND: + case ACCOUNT_LOCKED: + case ACCOUNT_LOCKED_PENDING: + // these are bad, we do not want to continue to log in and + // lock an account + // throw new InvalidLoginException(rc.getDesc()); + logger.error("Your MyQ Acount is invalid: {}", rc.getDesc()); + return new JsonObject(); + case LOGIN_ERROR: + // Our session key has expired, request a new one + if (retry) { + login(); + return request(method, url, payload, payloadType, false, login, securityToken, command); + } + // fall through to default + default: + logger.error("Request Failed: {}", rc.getDesc()); + return new JsonObject(); + } + + } catch (Exception e) { + logger.error("Could not parse response", e); + return new JsonObject(); + } + } + + /** + * Schedule our polling task + * + * @param millis + */ + private void schedulePoll(long millis) { + logger.trace("rapidRefreshFuture scheduling for {} millis", millis); + // start polling at the RAPID_REFRESH_SECS interval + pollFuture = pollService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + refreshDeviceState(); + } catch (Exception e) { + logger.trace("Exception occurred during refresh: {}", e.getMessage(), e); + } + } + }, 0, millis, TimeUnit.MILLISECONDS); + } + + /** + * Schedule the task to reset out poll rate in a future time + */ + private void scheduleFuturePollReset() { + // stop rapid polling after MAX_RAPID_REFRESH_SECS + pollResetFuture = pollService.schedule(new Runnable() { + @Override + public void run() { + logger.trace("rapidRefreshFutureEnd stopping"); + if (pollFuture != null && !pollFuture.isCancelled()) { + pollFuture.cancel(false); + } + } + }, MAX_RAPID_REFRESH, TimeUnit.MILLISECONDS); + } + + /** + * Start rapid polling + * + */ + private void beginRapidPoll() { + if (pollResetFuture != null) { + pollResetFuture.cancel(true); + pollResetFuture = null; + } + + if (pollResetFuture == null || pollResetFuture.isCancelled()) { + if (pollFuture == null || pollFuture.isCancelled()) { + schedulePoll(quickPoll * 1000); + } + scheduleFuturePollReset(); + } + } + + /** + * schedule a Poll in the near future + */ + private void doFuturePoll(long millis) { + pollFuture = pollService.schedule(new Runnable() { + @Override + public void run() { + logger.trace("do schedule poll"); + try { + refreshDeviceState(); + } catch (Exception e) { + logger.debug("Exception occurred during refresh: {}", e.getMessage(), e); + } + } + }, millis, TimeUnit.MILLISECONDS); + } +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQHandler.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQHandler.java new file mode 100644 index 000000000..115366874 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQHandler.java @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.handler; + +import static org.openhab.binding.chamberlainmyq.ChamberlainMyQBindingConstants.MYQ_SERIAL; + +import java.io.IOException; +import java.util.Iterator; + +import org.openhab.binding.chamberlainmyq.config.ChamberlainMyQDeviceConfig; +import org.openhab.binding.chamberlainmyq.handler.ChamberlainMyQGatewayHandler.RequestCallback; +import org.openhab.core.thing.Bridge; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +/** + * The {@link ChamberlainMyQHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Scott Hanson - Initial contribution + */ +public abstract class ChamberlainMyQHandler extends BaseThingHandler { + /** + * Base configuration of this device. + */ + protected ChamberlainMyQDeviceConfig deviceConfig; + + private Logger logger = LoggerFactory.getLogger(ChamberlainMyQHandler.class); + + /** + * Creates a new instance of this class for the {@link Thing}. + * + * @param thing the thing that should be handled, not null. + */ + public ChamberlainMyQHandler(Thing thing) { + super(thing); + + String id = getThing().getProperties().get(MYQ_SERIAL); + logger.debug("Thing ID: {}", id); + this.deviceConfig = new ChamberlainMyQDeviceConfig(getThing().getProperties()); + logger.info("Initializing a MyQ device: \n{}", deviceConfig.asString()); + } + + /** + * Parse the configuration of this thing. + * + * @param jsonConfigString the string containing the configuration of this thing as returned by the hub (in JSON). + */ + protected void parseConfig(String jsonConfigString) { + logger.debug("Parsing a thing's config: {}", jsonConfigString); + JsonParser parser = new JsonParser(); + deviceConfig.readConfigFromJson(parser.parse(jsonConfigString).getAsJsonObject()); + } + + /** + * Used to retrieve the {@link ChamberlainMyQGatewayHandler} controlling this thing. + * + * @return this thing gateway handler, null if it hasn't been set yet. + */ + protected ChamberlainMyQGatewayHandler getGatewayHandler() { + Bridge gateway = getBridge(); + return gateway == null ? null : (ChamberlainMyQGatewayHandler) gateway.getHandler(); + } + + /** + * Function called when a communication error between the gateway and the thing has been detected. + */ + protected void handleCommunicationError(String errorMessage) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage); + } + + private abstract class ChamberlainMyQDeviceRequestCallback implements RequestCallback { + /** + * Handler of the thing that should for which the configuration should be set. + */ + protected ChamberlainMyQHandler handler; + + /** + * Creates a new instance of this class. + * + * @param handler The handler for which the configuration should be read. + */ + public ChamberlainMyQDeviceRequestCallback(ChamberlainMyQHandler handler) { + if (handler == null) { + throw new IllegalArgumentException("The argument |handler| must not be null."); + } + // checkArgument(handler != null, "The handler must not be null"); + // Preconditions.checkArgument(handler != null, "The argument |handler| must not be null."); + this.handler = handler; + } + + @Override + public void onError(String error) { + handler.handleCommunicationError(error); + } + } + + //////////// Read State functions //////////// + + /** + * Specialization of a {link RequestCallback} to read a device configuration. + * + * @author Scott Hanson + */ + private class ReadDeviceStateCallback extends ChamberlainMyQDeviceRequestCallback { + /** + * Creates a new instance of this class. + * + * @param handler The handler for which the state should be read. + */ + public ReadDeviceStateCallback(ChamberlainMyQHandler handler) { + super(handler); + } + + @Override + public void parseRequestResult(JsonObject jsonResult) { + logger.trace("Parsing a ReadDeviceState request result: {}", jsonResult); + // The response from the server is a JSON object containing the device information and state. + handler.updateDeviceStateCallback(jsonResult); + } + } + + /** + * Query the {@link MyQGatewayHandler} for this device's state. + */ + protected void readDeviceState() { + try { + getGatewayHandler().sendRequestToServer(new ReadDeviceStateCallback(this)); + } catch (IOException e) { + logger.error("Error while querying the hub for {}", e.getMessage()); + } + } + + /** + * Callback called once the device state has been updated. + * + * @param jsonDataBlob the reply from the gateway. + */ + protected void updateDeviceStateCallback(JsonObject jsonDataBlob) { + String deviceSerial = getThing().getProperties().get(MYQ_SERIAL); + JsonElement deviceData = jsonDataBlob.get("items"); + Iterator deviceDataIter = deviceData.getAsJsonArray().iterator(); + while (deviceDataIter.hasNext()) { + JsonElement element = deviceDataIter.next(); + if (!element.isJsonObject()) { + continue; + } + if (element.getAsJsonObject().get(MYQ_SERIAL) != null) { + String findDeviceSerial = element.getAsJsonObject().get(MYQ_SERIAL).getAsString(); + if (deviceSerial.compareTo(findDeviceSerial) == 0) { + updateState(element.getAsJsonObject()); + } + } + } + } + + protected abstract void updateState(JsonObject jsonDataBlob); +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQLightHandler.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQLightHandler.java new file mode 100644 index 000000000..14153a496 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/handler/ChamberlainMyQLightHandler.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.handler; + +import static org.openhab.binding.chamberlainmyq.ChamberlainMyQBindingConstants.*; + +import java.io.IOException; + +import org.openhab.binding.chamberlainmyq.internal.InvalidLoginException; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +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.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonObject; + +/** + * The {@link ChamberlainMyQLightHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Scott Hanson - Initial contribution + */ +public class ChamberlainMyQLightHandler extends ChamberlainMyQHandler { + + private Logger logger = LoggerFactory.getLogger(ChamberlainMyQLightHandler.class); + + public ChamberlainMyQLightHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + if (!this.deviceConfig.validateConfig()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid config."); + return; + } + + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void channelLinked(ChannelUID channelUID) { + if (channelUID.getId().equals(CHANNEL_LIGHT_STATE) || channelUID.getId().equals(CHANNEL_SERIAL_NUMBER) + || channelUID.getId().equals(CHANNEL_NAME) || channelUID.getId().equals(CHANNEL_TYPE)) { + readDeviceState(); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (channelUID.getId().equals(CHANNEL_LIGHT_STATE)) { + if (command.equals(OnOffType.ON)) { + setLightState(true); + } else if (command.equals(OnOffType.OFF)) { + setLightState(false); + } else if (command instanceof RefreshType) { + readDeviceState(); + } + } + } + + private void setLightState(boolean state) { + String deviceSerial = getThing().getProperties().get(MYQ_SERIAL); + try { + if (state) { + getGatewayHandler().executeMyQCommand(deviceSerial, "turnon", false); + } else { + getGatewayHandler().executeMyQCommand(deviceSerial, "turnoff", false); + } + } catch (InvalidLoginException e) { + logger.error("Error Setting Light State: {}", e.getMessage()); + } catch (IOException e) { + logger.error("Error Setting Light State: {}", e.getMessage()); + } + } + + @Override + public void updateState(JsonObject jsonDataBlob) { + deviceConfig.readConfigFromJson(jsonDataBlob); + logger.trace("updateState: {}", deviceConfig.getSerialNumber()); + updateState(CHANNEL_LIGHT_STATE, deviceConfig.getLightStatusOnOff()); + updateState(CHANNEL_NAME, StringType.valueOf(deviceConfig.getName())); + updateState(CHANNEL_SERIAL_NUMBER, StringType.valueOf(deviceConfig.getSerialNumber())); + updateState(CHANNEL_TYPE, StringType.valueOf(deviceConfig.getDeviceType())); + updateStatus(deviceConfig.getThingOnline()); + } +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/ChamberlainMyQHandlerFactory.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/ChamberlainMyQHandlerFactory.java new file mode 100644 index 000000000..52af49ae9 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/ChamberlainMyQHandlerFactory.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.internal; + +import static org.openhab.binding.chamberlainmyq.ChamberlainMyQBindingConstants.*; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.openhab.binding.chamberlainmyq.handler.ChamberlainMyQDoorOpenerHandler; +import org.openhab.binding.chamberlainmyq.handler.ChamberlainMyQGatewayHandler; +import org.openhab.binding.chamberlainmyq.handler.ChamberlainMyQLightHandler; +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.Component; + +/** + * The {@link ChamberlainMyQHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Scott Hanson - Initial contribution + */ +@Component(configurationPid = "binding.chamberlainmyq", service = ThingHandlerFactory.class) +public class ChamberlainMyQHandlerFactory extends BaseThingHandlerFactory { + + public static final Set DISCOVERABLE_DEVICE_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_DOOR_OPENER, THING_TYPE_LIGHT).collect(Collectors.toSet())); + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_BRIDGE); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID) + || DISCOVERABLE_DEVICE_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (thingTypeUID.equals(THING_TYPE_BRIDGE)) { + return new ChamberlainMyQGatewayHandler((Bridge) thing); + } else if (thingTypeUID.equals(THING_TYPE_DOOR_OPENER)) { + return new ChamberlainMyQDoorOpenerHandler(thing); + } else if (thingTypeUID.equals(THING_TYPE_LIGHT)) { + return new ChamberlainMyQLightHandler(thing); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/ChamberlainMyQResponseCode.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/ChamberlainMyQResponseCode.java new file mode 100644 index 000000000..cdba2be02 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/ChamberlainMyQResponseCode.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.internal; + +/** + * Enum of error codes from Chamberlain the MyQ API + * + * @see http + * ://chamberlain.custhelp.com/app/answers/detail/a_id/4922/~/what-are-the + * -myq-error-codes%3F + * @author Dan Cunningham - Initial contribution + * @author Scott Hanson - 2.x Binding + * + */ +public enum ChamberlainMyQResponseCode { + + OK(0, "OK"), + LOGIN_ERROR(401, "Login error. Please login again."), // token has expired + ACCOUNT_INVALID(203, "The username or password you entered is incorrect. Try again."), + ACCOUNT_NOT_FOUND(204, "The username was not found or is locked out."), + ACCOUNT_LOCKED_PENDING(205, "This user will be locked out."), + ACCOUNT_LOCKED(207, "This user is locked out."), + GATEWAY_OFFLINE(223, "Gateway is Offline."), + GATEWAY_LEARNMODE(224, "Gateway is in learn mode."), + DEVICE_LEARNMODE(305, "The device is currently in Learn Mode."), + DEVICE_NOT_RESPONDING(308, "The device is not responding. Please check that the device is powered and in range."), + DEVICE_OFFLINE(309, "The gateway or hub is offline. Please check the power and network connections."), + UNKNOWN(-1, "Unknow resonse"); + + private int code; + private String desc; + + /** + * Creates a new Response code object from the numeric code + * + * @param code + * @param desc + */ + private ChamberlainMyQResponseCode(int code, String desc) { + this.code = code; + this.desc = desc; + } + + /** + * The human readable description of the error + * + * @return + */ + public String getDesc() { + return desc; + } + + /** + * The error code number from the API. + * + * @return + */ + public int getCode() { + return code; + } + + /** + * Return a MyQResponseCode from a given code number + * + * @param code + * @return + */ + public static ChamberlainMyQResponseCode fromCode(int code) { + for (ChamberlainMyQResponseCode rc : values()) { + if (rc.getCode() == code) { + return rc; + } + } + return UNKNOWN; + } +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/HttpUtil.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/HttpUtil.java new file mode 100644 index 000000000..96544fe1f --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/HttpUtil.java @@ -0,0 +1,178 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.InputStreamContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Need to Subclass to override the USERAGENT ID to spoof the MyQ App + * Some common methods to be used in both HTTP-In-Binding and HTTP-Out-Binding + * + * @author Scott Hanson - Initial contribution + * @author Thomas Eichstaedt-Engelen + * @author Kai Kreuzer - Initial contribution and API + * @author Svilen Valkanov - replaced Apache HttpClient with Jetty + */ +public class HttpUtil { + + private static Logger logger = LoggerFactory.getLogger(HttpUtil.class); + + private static HttpClient client = new HttpClient(new SslContextFactory()); + + /** + * Executes the given url with the given httpMethod. + * Furthermore the http.proxyXXX System variables are read and + * set into the {@link HttpClient}. + * + * @param httpMethod the HTTP method to use + * @param url the url to execute (in milliseconds) + * @param timeout the socket timeout to wait for data + * + * @return the response body or NULL when the request went wrong + * @throws IOException when the request execution failed, timed out or it was interrupted + */ + public static String executeUrl(String httpMethod, String url, int timeout, boolean shouldReturn) + throws IOException { + return executeUrl(httpMethod, url, null, null, timeout, shouldReturn); + } + + /** + * Executes the given url with the given httpMethod. + * Furthermore the http.proxyXXX System variables are read and + * set into the {@link HttpClient}. + * + * @param httpMethod the HTTP method to use + * @param url the url to execute (in milliseconds) + * @param content the content to be send to the given url or null if no content should + * be + * send. + * @param contentType the content type of the given content + * @param timeout the socket timeout to wait for data + * + * @return the response body or NULL when the request went wrong + * @throws IOException when the request execution failed, timed out or it was interrupted + */ + public static String executeUrl(String httpMethod, String url, InputStream content, String contentType, int timeout, + boolean shouldReturn) throws IOException { + return executeUrl(httpMethod, url, null, content, contentType, timeout, shouldReturn); + } + + /** + * Executes the given url with the given httpMethod + * + * @param httpMethod the HTTP method to use + * @param url the url to execute (in milliseconds) + * @param httpHeaders optional HTTP headers which has to be set on request + * @param content the content to be send to the given url or null if no content should + * be + * send. + * @param contentType the content type of the given content + * @param timeout the socket timeout to wait for data + * @return the response body or NULL when the request went wrong + * @throws IOException when the request execution failed, timed out or it was interrupted + */ + public static String executeUrl(String httpMethod, String url, Properties httpHeaders, InputStream content, + String contentType, int timeout, boolean shouldReturn) throws IOException { + startHttpClient(client); + + // client.setUserAgentField(new HttpField(HttpHeader.USER_AGENT, USERAGENT)); + + HttpMethod method = HttpUtil.createHttpMethod(httpMethod); + + Request request = client.newRequest(url).method(method).timeout(timeout, TimeUnit.MILLISECONDS); + + if (httpHeaders != null) { + for (String httpHeaderKey : httpHeaders.stringPropertyNames()) { + request.header(httpHeaderKey, httpHeaders.getProperty(httpHeaderKey)); + } + } + + // add content if a valid method is given ... + if (content != null && (method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT))) { + request.content(new InputStreamContentProvider(content), contentType); + } + + logger.trace("About to execute {}", request.getURI()); + + try { + ContentResponse response = request.send(); + int statusCode = response.getStatus(); + if (statusCode >= HttpStatus.BAD_REQUEST_400) { + String statusLine = statusCode + " " + response.getReason(); + logger.debug("Method failed: {}", statusLine); + } + + if (!shouldReturn) + return null; + + byte[] rawResponse = response.getContent(); + String encoding = response.getEncoding() != null ? response.getEncoding().replaceAll("\"", "").trim() + : "UTF-8"; + String responseBody = new String(rawResponse, encoding); + if (!responseBody.isEmpty()) { + logger.trace("Http Response {}.", responseBody); + } + + return responseBody; + } catch (Exception e) { + throw new IOException(e); + } finally { + } + } + + /** + * Factory method to create a {@link HttpMethod}-object according to the + * given String httpMethodString + * + * @param httpMethodString the name of the {@link HttpMethod} to create + * + * @throws IllegalArgumentException if httpMethod is none of GET, PUT, + * POST or DELETE + */ + public static HttpMethod createHttpMethod(String httpMethodString) { + if ("GET".equals(httpMethodString)) { + return HttpMethod.GET; + } else if ("PUT".equals(httpMethodString)) { + return HttpMethod.PUT; + } else if ("POST".equals(httpMethodString)) { + return HttpMethod.POST; + } else if ("DELETE".equals(httpMethodString)) { + return HttpMethod.DELETE; + } else { + throw new IllegalArgumentException("given httpMethod '" + httpMethodString + "' is unknown"); + } + } + + private static void startHttpClient(HttpClient client) { + if (!client.isStarted()) { + try { + client.start(); + } catch (Exception e) { + logger.warn("Cannot start HttpClient!", e); + } + } + } +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/InvalidDataException.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/InvalidDataException.java new file mode 100644 index 000000000..333445ba4 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/InvalidDataException.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.internal; + +import java.io.IOException; + +/** + * Throw if the data we are parsing in not what we are expecting for input. + * + * @author Dan Cunningham - Initial contribution + * @author Scott Hanson - 2.x Binding + * + */ +public class InvalidDataException extends IOException { + + private static final long serialVersionUID = 1L; + + public InvalidDataException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/InvalidLoginException.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/InvalidLoginException.java new file mode 100644 index 000000000..be02b369e --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/InvalidLoginException.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.internal; + +/** + * Exception type used when a login attempt fails against the MyQ API. + * + * @author Dan Cunningham - Initial contribution + * @author Scott Hanson - 2.x Binding + * + */ +public class InvalidLoginException extends Exception { + + private static final long serialVersionUID = 1L; + + public InvalidLoginException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/discovery/ChamberlainMyQDeviceDiscoveryService.java b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/discovery/ChamberlainMyQDeviceDiscoveryService.java new file mode 100644 index 000000000..61091d4cb --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/java/org/openhab/binding/chamberlainmyq/internal/discovery/ChamberlainMyQDeviceDiscoveryService.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2010-2020 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.chamberlainmyq.internal.discovery; + +import static org.openhab.binding.chamberlainmyq.ChamberlainMyQBindingConstants.*; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.openhab.binding.chamberlainmyq.config.ChamberlainMyQDeviceConfig; +import org.openhab.binding.chamberlainmyq.handler.ChamberlainMyQGatewayHandler; +import org.openhab.binding.chamberlainmyq.handler.ChamberlainMyQGatewayHandler.RequestCallback; +import org.openhab.binding.chamberlainmyq.internal.ChamberlainMyQHandlerFactory; +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.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * Send DiscoveryService Callbacks + * + * @author Scott Hanson - Initial contribution + */ + +public class ChamberlainMyQDeviceDiscoveryService extends AbstractDiscoveryService { + private final Logger logger = LoggerFactory.getLogger(ChamberlainMyQDeviceDiscoveryService.class); + private ChamberlainMyQGatewayHandler hubHandler; + + public ChamberlainMyQDeviceDiscoveryService(ChamberlainMyQGatewayHandler hubHandler) + throws IllegalArgumentException { + super(ChamberlainMyQHandlerFactory.DISCOVERABLE_DEVICE_TYPES_UIDS, 10); + + this.hubHandler = hubHandler; + } + + private ScheduledFuture scanTask; + + @Override + protected void startScan() { + if (this.scanTask == null || this.scanTask.isDone()) { + this.scanTask = scheduler.schedule(new Runnable() { + @Override + public void run() { + try { + readDeviceDatabase(); + } catch (Exception e) { + logger.error("Error scanning for devices", e); + + if (scanListener != null) { + scanListener.onErrorOccurred(e); + } + } + } + }, 0, TimeUnit.SECONDS); + } + } + + protected void addMyQDevice(ThingTypeUID thinkType, ChamberlainMyQDeviceConfig config) { + logger.debug("New Device {}", config.toString()); + + ThingUID hubUID = this.hubHandler.getThing().getUID(); + ThingUID uid = new ThingUID(thinkType, hubUID, config.getSerialNumber()); + + Map properties = config.getProperties(); + DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(config.getName()) + .withProperties(properties).withBridge(hubUID).build(); + + thingDiscovered(result); + + logger.debug("Discovered {}", uid); + } + + protected void enumerateDevices(JsonObject gatewayResponse) { + JsonElement deviceData = gatewayResponse.get("items"); + if (deviceData == null) { + logger.error("Empty Device Data"); + return; + } + logger.debug("Chamberlain MyQ Devices:"); + Iterator deviceDataIter = deviceData.getAsJsonArray().iterator(); + while (deviceDataIter.hasNext()) { + JsonElement element = deviceDataIter.next(); + if (!element.isJsonObject()) { + continue; + } + if (element.getAsJsonObject().get(MYQ_SERIAL) != null) { + ChamberlainMyQDeviceConfig config = new ChamberlainMyQDeviceConfig(element.getAsJsonObject()); + + String deviceFamily = config.getDeviceFamily(); + logger.debug("New Device {}", config.getDeviceFamily()); + if (deviceFamily.compareTo("garagedoor") == 0) { + addMyQDevice(THING_TYPE_DOOR_OPENER, config); + } else if (deviceFamily.compareTo("lamp") == 0) { + addMyQDevice(THING_TYPE_LIGHT, config); + } + } + } + } + + private class ListDevicesCallback implements RequestCallback { + private ChamberlainMyQDeviceDiscoveryService discoveryService; + + public ListDevicesCallback(ChamberlainMyQDeviceDiscoveryService discoveryService) { + this.discoveryService = discoveryService; + } + + @Override + public void parseRequestResult(JsonObject jsonResult) { + discoveryService.enumerateDevices(jsonResult); + } + + @Override + public void onError(String error) { + discoveryService.logger.error("Error during the device discovery: {}", error); + } + } + + private void readDeviceDatabase() throws IOException { + try { + hubHandler.sendRequestToServer(new ListDevicesCallback(this)); + } catch (IOException e) { + logger.error("Error while querying the hub for the devices. ", e); + } + } +} diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 000000000..eb9b94f4c --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + ChamberlainMyQ Binding + This is the binding for ChamberlainMyQ. + Scott Hanson + + diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/MyQDoorOpener.xml b/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/MyQDoorOpener.xml new file mode 100644 index 000000000..7cdbf6e3a --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/MyQDoorOpener.xml @@ -0,0 +1,22 @@ + + + + + + + + Garage Door Opener + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/MyQGateway.xml b/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/MyQGateway.xml new file mode 100644 index 000000000..f1dbf3d4d --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/MyQGateway.xml @@ -0,0 +1,36 @@ + + + + + Web API for MyQ Devices + + + + The user name to log in to the MyQ App + + + password + + The user password to log in to the MyQ App + + + + HTTP requests timeout(sec). + 25 + + + + The Poll Period(sec). + 60 + + + + The Poll Period after an event is triggered(sec). + 2 + + + + diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/MyQLight.xml b/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/MyQLight.xml new file mode 100644 index 000000000..1ddee7158 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/MyQLight.xml @@ -0,0 +1,18 @@ + + + + + + + + On/off switch + + + + + + + diff --git a/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/channels.xml new file mode 100644 index 000000000..2323fb023 --- /dev/null +++ b/bundles/org.openhab.binding.chamberlainmyq/src/main/resources/OH-INF/thing/channels.xml @@ -0,0 +1,54 @@ + + + + Switch + + On/Off Status of the Light + Switch + + + Switch + + On/Off Status of the Garage Door + Switch + + + Rollershutter + + Roller Status of the Garage Door + Switch + + + String + + Status of the Garage Door + + + + Contact + + Garage Door Open + + + + Contact + + Garage Door Closed + + + + String + + Device Type + + + + String + + Device Name + + +