[myq] Remove MyQ Binding (#15911)
Fixes #15910 Signed-off-by: Dan Cunningham <dan@digitaldan.com>
This commit is contained in:
parent
45786fa12c
commit
01ab38ee13
|
@ -226,7 +226,6 @@
|
|||
/bundles/org.openhab.binding.mycroft/ @dalgwen
|
||||
/bundles/org.openhab.binding.mybmw/ @weymann @ntruchsess
|
||||
/bundles/org.openhab.binding.mynice/ @clinique
|
||||
/bundles/org.openhab.binding.myq/ @digitaldan
|
||||
/bundles/org.openhab.binding.mystrom/ @pail23
|
||||
/bundles/org.openhab.binding.nanoleaf/ @stefan-hoehn
|
||||
/bundles/org.openhab.binding.neato/ @jjlauterbach
|
||||
|
|
|
@ -1116,11 +1116,6 @@
|
|||
<artifactId>org.openhab.binding.mynice</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.myq</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.mystrom</artifactId>
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
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
|
||||
|
||||
== Third-party Content
|
||||
|
||||
jsoup
|
||||
* License: MIT License
|
||||
* Project: https://jsoup.org/
|
||||
* Source: https://github.com/jhy/jsoup
|
|
@ -1,72 +0,0 @@
|
|||
# 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% |
|
||||
| closeError | Switch | garagedoor | ON (has error), OFF (doesn't have error) |
|
||||
| openError | Switch | garagedoor | ON (has error), OFF (doesn't have error) |
|
||||
| switch | Switch | garagedoor, lamp | ON (open), OFF (closed) |
|
||||
|
||||
## Full Example
|
||||
|
||||
### Thing Configuration
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
String MyQGarageDoor1Status "Door Status [%s]" {channel = "myq:garagedoor:home:abcd12345:status"}
|
||||
Switch MyQGarageDoor1Switch "Door Switch [%s]" {channel = "myq:garagedoor:home:abcd12345:switch"}
|
||||
Switch MyQGarageDoor1CloseError "Door Close Error [%s]" {channel = "myq:garagedoor:home:abcd12345:closeError"}
|
||||
Switch MyQGarageDoor1OpenError "Door OpenError [%s]" {channel = "myq:garagedoor:home:abcd12345:openError"}
|
||||
Rollershutter MyQGarageDoor1Rollershutter "Door Rollershutter [%s]" {channel = "myq:garagedoor:home:abcd12345:rollershutter"}
|
||||
Switch MyQGarageDoorLamp "Lamp [%s]" {channel = "myq:lamp:home:efgh6789:switch"}
|
||||
}
|
||||
```
|
||||
|
||||
### Sitemaps
|
||||
|
||||
```perl
|
||||
sitemap MyQ label="MyQ Demo Sitemap" {
|
||||
Frame label="Garage Door" {
|
||||
String item=MyQGarageDoor1Status
|
||||
Switch item=MyQGarageDoor1Switch
|
||||
Rollershutter item=MyQGarageDoor1Rollershutter
|
||||
}
|
||||
}
|
||||
```
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.myq</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: MyQ Binding</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.14.3</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.myq-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-myq" description="MyQ Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle dependency="true">mvn:org.jsoup/jsoup/1.14.3</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.myq/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myq.internal;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link MyQBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MyQBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "myq";
|
||||
|
||||
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
|
||||
public static final ThingTypeUID THING_TYPE_GARAGEDOOR = new ThingTypeUID(BINDING_ID, "garagedoor");
|
||||
public static final ThingTypeUID THING_TYPE_LAMP = new ThingTypeUID(BINDING_ID, "lamp");
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_GARAGEDOOR,
|
||||
THING_TYPE_LAMP);
|
||||
public static final Set<ThingTypeUID> SUPPORTED_DISCOVERY_THING_TYPES_UIDS = Set.of(THING_TYPE_GARAGEDOOR,
|
||||
THING_TYPE_LAMP);
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.myq.internal.dto.DeviceDTO;
|
||||
import org.openhab.binding.myq.internal.handler.MyQAccountHandler;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
|
||||
/**
|
||||
* The {@link MyQDiscoveryService} is responsible for discovering MyQ things
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MyQDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_DISCOVERY_THING_TYPES_UIDS = Set
|
||||
.of(MyQBindingConstants.THING_TYPE_GARAGEDOOR, MyQBindingConstants.THING_TYPE_LAMP);
|
||||
private @Nullable MyQAccountHandler accountHandler;
|
||||
|
||||
public MyQDiscoveryService() {
|
||||
super(SUPPORTED_DISCOVERY_THING_TYPES_UIDS, 1, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypes() {
|
||||
return SUPPORTED_DISCOVERY_THING_TYPES_UIDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startScan() {
|
||||
MyQAccountHandler accountHandler = this.accountHandler;
|
||||
if (accountHandler != null) {
|
||||
List<DeviceDTO> devices = accountHandler.devicesCache();
|
||||
if (devices != null) {
|
||||
devices.forEach(device -> {
|
||||
ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, device.deviceFamily);
|
||||
if (SUPPORTED_DISCOVERY_THING_TYPES_UIDS.contains(thingTypeUID)) {
|
||||
ThingUID thingUID = new ThingUID(thingTypeUID, accountHandler.getThing().getUID(),
|
||||
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 startBackgroundDiscovery() {
|
||||
startScan();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(ThingHandler handler) {
|
||||
if (handler instanceof MyQAccountHandler myqAccountHandler) {
|
||||
accountHandler = myqAccountHandler;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return accountHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activate() {
|
||||
super.activate(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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.auth.client.oauth2.OAuthFactory;
|
||||
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;
|
||||
private OAuthFactory oAuthFactory;
|
||||
|
||||
@Activate
|
||||
public MyQHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
|
||||
final @Reference OAuthFactory oAuthFactory) {
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
this.oAuthFactory = oAuthFactory;
|
||||
}
|
||||
|
||||
@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, oAuthFactory);
|
||||
}
|
||||
|
||||
if (THING_TYPE_GARAGEDOOR.equals(thingTypeUID)) {
|
||||
return new MyQGarageDoorHandler(thing);
|
||||
}
|
||||
|
||||
if (THING_TYPE_LAMP.equals(thingTypeUID)) {
|
||||
return new MyQLampHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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 = "";
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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 String id;
|
||||
public String name;
|
||||
public String createdBy;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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 AccountsDTO} entity from the MyQ API
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
*/
|
||||
public class AccountsDTO {
|
||||
public List<AccountDTO> accounts;
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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 String accountId;
|
||||
public DeviceStateDTO state;
|
||||
public String parentDevice;
|
||||
public String parentDeviceId;
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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 DeviceStateDTO} entity from the MyQ API
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
*/
|
||||
public class DeviceStateDTO {
|
||||
|
||||
public Boolean gdoLockConnected;
|
||||
public Boolean attachedWorkLightErrorPresent;
|
||||
public String learnStatus;
|
||||
public Boolean hasCamera;
|
||||
public String lampState;
|
||||
public String batteryBackupState;
|
||||
public String doorState;
|
||||
public String lastUpdate;
|
||||
public Boolean isUnattendedOpenAllowed;
|
||||
public Boolean isUnattendedCloseAllowed;
|
||||
public Integer serviceCycleCount;
|
||||
public Integer absoluteCycleCount;
|
||||
public Boolean online;
|
||||
public String lastStatus;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.myq.internal.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The {@link DevicesDTO} entity from the MyQ API
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
*/
|
||||
public class DevicesDTO {
|
||||
public String href;
|
||||
public Integer count;
|
||||
public List<DeviceDTO> items;
|
||||
}
|
|
@ -1,675 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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.io.IOException;
|
||||
import java.net.CookieStore;
|
||||
import java.net.HttpCookie;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpContentResponse;
|
||||
import org.eclipse.jetty.client.api.ContentProvider;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.api.Result;
|
||||
import org.eclipse.jetty.client.util.BufferingResponseListener;
|
||||
import org.eclipse.jetty.client.util.FormContentProvider;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.openhab.binding.myq.internal.MyQDiscoveryService;
|
||||
import org.openhab.binding.myq.internal.config.MyQAccountConfiguration;
|
||||
import org.openhab.binding.myq.internal.dto.AccountDTO;
|
||||
import org.openhab.binding.myq.internal.dto.AccountsDTO;
|
||||
import org.openhab.binding.myq.internal.dto.DeviceDTO;
|
||||
import org.openhab.binding.myq.internal.dto.DevicesDTO;
|
||||
import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
|
||||
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthClientService;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthException;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthFactory;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
|
||||
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 implements AccessTokenRefreshListener {
|
||||
private static final int REQUEST_TIMEOUT_MS = 10_000;
|
||||
|
||||
/*
|
||||
* MyQ oAuth relate fields
|
||||
*/
|
||||
private static final String CLIENT_SECRET = "VUQ0RFhuS3lQV3EyNUJTdw==";
|
||||
private static final String CLIENT_ID = "ANDROID_CGI_MYQ";
|
||||
private static final String REDIRECT_URI = "com.myqops://android";
|
||||
private static final String SCOPE = "MyQ_Residential offline_access";
|
||||
/*
|
||||
* MyQ authentication API endpoints
|
||||
*/
|
||||
private static final String LOGIN_BASE_URL = "https://partner-identity.myq-cloud.com";
|
||||
private static final String LOGIN_AUTHORIZE_URL = LOGIN_BASE_URL + "/connect/authorize";
|
||||
private static final String LOGIN_TOKEN_URL = LOGIN_BASE_URL + "/connect/token";
|
||||
// this should never happen, but lets be safe and give up after so many redirects
|
||||
private static final int LOGIN_MAX_REDIRECTS = 30;
|
||||
/*
|
||||
* MyQ device and account API endpoints
|
||||
*/
|
||||
private static final String ACCOUNTS_URL = "https://accounts.myq-cloud.com/api/v6.0/accounts";
|
||||
private static final String DEVICES_URL = "https://devices.myq-cloud.com/api/v5.2/Accounts/%s/Devices";
|
||||
private static final String CMD_LAMP_URL = "https://account-devices-lamp.myq-cloud.com/api/v5.2/Accounts/%s/lamps/%s/%s";
|
||||
private static final String CMD_DOOR_URL = "https://account-devices-gdo.myq-cloud.com/api/v5.2/Accounts/%s/door_openers/%s/%s";
|
||||
|
||||
private static final Integer RAPID_REFRESH_SECONDS = 5;
|
||||
private final Logger logger = LoggerFactory.getLogger(MyQAccountHandler.class);
|
||||
private final Gson gsonLowerCase = new GsonBuilder()
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
|
||||
private final OAuthFactory oAuthFactory;
|
||||
private @Nullable Future<?> normalPollFuture;
|
||||
private @Nullable Future<?> rapidPollFuture;
|
||||
private @Nullable AccountsDTO accounts;
|
||||
|
||||
private List<DeviceDTO> devicesCache = new ArrayList<DeviceDTO>();
|
||||
private @Nullable OAuthClientService oAuthService;
|
||||
private Integer normalRefreshSeconds = 60;
|
||||
private HttpClient httpClient;
|
||||
private String username = "";
|
||||
private String password = "";
|
||||
private String userAgent = "";
|
||||
// force login, even if we have a token
|
||||
private boolean needsLogin = false;
|
||||
|
||||
public MyQAccountHandler(Bridge bridge, HttpClient httpClient, final OAuthFactory oAuthFactory) {
|
||||
super(bridge);
|
||||
this.httpClient = httpClient;
|
||||
this.oAuthFactory = oAuthFactory;
|
||||
}
|
||||
|
||||
@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 = ""; // no agent string
|
||||
needsLogin = true;
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
restartPolls(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopPolls();
|
||||
OAuthClientService oAuthService = this.oAuthService;
|
||||
if (oAuthService != null) {
|
||||
oAuthService.removeAccessTokenRefreshListener(this);
|
||||
oAuthFactory.ungetOAuthService(getThing().toString());
|
||||
this.oAuthService = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRemoval() {
|
||||
oAuthFactory.deleteServiceAndAccessToken(getThing().toString());
|
||||
super.handleRemoval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Set.of(MyQDiscoveryService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
|
||||
List<DeviceDTO> localDeviceCaches = devicesCache;
|
||||
if (childHandler instanceof MyQDeviceHandler deviceHandler) {
|
||||
localDeviceCaches.stream().filter(d -> deviceHandler.getSerialNumber().equalsIgnoreCase(d.serialNumber))
|
||||
.findFirst().ifPresent(deviceHandler::handleDeviceUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {
|
||||
logger.debug("Auth Token Refreshed, expires in {}", tokenResponse.getExpiresIn());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a door action to the MyQ API
|
||||
*
|
||||
* @param device
|
||||
* @param action
|
||||
*/
|
||||
public void sendDoorAction(DeviceDTO device, String action) {
|
||||
sendAction(device, action, CMD_DOOR_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a lamp action to the MyQ API
|
||||
*
|
||||
* @param device
|
||||
* @param action
|
||||
*/
|
||||
public void sendLampAction(DeviceDTO device, String action) {
|
||||
sendAction(device, action, CMD_LAMP_URL);
|
||||
}
|
||||
|
||||
private void sendAction(DeviceDTO device, String action, String urlFormat) {
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
logger.debug("Account offline, ignoring action {}", action);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ContentResponse response = sendRequest(
|
||||
String.format(urlFormat, device.accountId, device.serialNumber, action), HttpMethod.PUT, null,
|
||||
null);
|
||||
if (HttpStatus.isSuccess(response.getStatus())) {
|
||||
restartPolls(true);
|
||||
} else {
|
||||
logger.debug("Failed to send action {} : {}", action, response.getContentAsString());
|
||||
}
|
||||
} catch (InterruptedException | MyQCommunicationException | MyQAuthenticationException e) {
|
||||
logger.debug("Could not send action", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Last known state of MyQ Devices
|
||||
*
|
||||
* @return cached MyQ devices
|
||||
*/
|
||||
public @Nullable List<DeviceDTO> 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 (accounts == null) {
|
||||
getAccounts();
|
||||
}
|
||||
getDevices();
|
||||
} catch (MyQCommunicationException e) {
|
||||
logger.debug("MyQ communication error", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
} catch (MyQAuthenticationException e) {
|
||||
logger.debug("MyQ authentication error", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
stopPolls();
|
||||
} catch (InterruptedException e) {
|
||||
// we were shut down, ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This attempts to navigate the MyQ oAuth login flow in order to obtain a @AccessTokenResponse
|
||||
*
|
||||
* @return AccessTokenResponse token
|
||||
* @throws InterruptedException
|
||||
* @throws MyQCommunicationException
|
||||
* @throws MyQAuthenticationException
|
||||
*/
|
||||
private AccessTokenResponse login()
|
||||
throws InterruptedException, MyQCommunicationException, MyQAuthenticationException {
|
||||
try {
|
||||
// make sure we have a fresh session
|
||||
URI authUri = new URI(LOGIN_BASE_URL);
|
||||
CookieStore store = httpClient.getCookieStore();
|
||||
store.get(authUri).forEach(cookie -> {
|
||||
store.remove(authUri, cookie);
|
||||
});
|
||||
|
||||
String codeVerifier = generateCodeVerifier();
|
||||
|
||||
ContentResponse loginPageResponse = getLoginPage(codeVerifier);
|
||||
|
||||
// load the login page to get cookies and form parameters
|
||||
Document loginPage = Jsoup.parse(loginPageResponse.getContentAsString());
|
||||
Element form = loginPage.select("form").first();
|
||||
Element requestToken = loginPage.select("input[name=__RequestVerificationToken]").first();
|
||||
Element returnURL = loginPage.select("input[name=ReturnUrl]").first();
|
||||
|
||||
if (form == null || requestToken == null) {
|
||||
throw new MyQCommunicationException("Could not load login page");
|
||||
}
|
||||
|
||||
// url that the form will submit to
|
||||
String action = LOGIN_BASE_URL + form.attr("action");
|
||||
|
||||
// post our user name and password along with elements from the scraped form
|
||||
String location = postLogin(action, requestToken.attr("value"), returnURL.attr("value"));
|
||||
if (location == null) {
|
||||
throw new MyQAuthenticationException("Could not login with credentials");
|
||||
}
|
||||
|
||||
// finally complete the oAuth flow and retrieve a JSON oAuth token response
|
||||
ContentResponse tokenResponse = getLoginToken(location, codeVerifier);
|
||||
String loginToken = tokenResponse.getContentAsString();
|
||||
|
||||
try {
|
||||
AccessTokenResponse accessTokenResponse = gsonLowerCase.fromJson(loginToken, AccessTokenResponse.class);
|
||||
if (accessTokenResponse == null) {
|
||||
throw new MyQAuthenticationException("Could not parse token response");
|
||||
}
|
||||
getOAuthService().importAccessTokenResponse(accessTokenResponse);
|
||||
return accessTokenResponse;
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new MyQCommunicationException("Invalid Token Response " + loginToken);
|
||||
}
|
||||
} catch (IOException | ExecutionException | TimeoutException | OAuthException | URISyntaxException e) {
|
||||
throw new MyQCommunicationException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void getAccounts() throws InterruptedException, MyQCommunicationException, MyQAuthenticationException {
|
||||
ContentResponse response = sendRequest(ACCOUNTS_URL, HttpMethod.GET, null, null);
|
||||
accounts = parseResultAndUpdateStatus(response, gsonLowerCase, AccountsDTO.class);
|
||||
}
|
||||
|
||||
private void getDevices() throws InterruptedException, MyQCommunicationException, MyQAuthenticationException {
|
||||
AccountsDTO localAccounts = accounts;
|
||||
if (localAccounts == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<DeviceDTO> currentDevices = new ArrayList<DeviceDTO>();
|
||||
|
||||
for (AccountDTO account : localAccounts.accounts) {
|
||||
ContentResponse response = sendRequest(String.format(DEVICES_URL, account.id), HttpMethod.GET, null, null);
|
||||
DevicesDTO devices = parseResultAndUpdateStatus(response, gsonLowerCase, DevicesDTO.class);
|
||||
currentDevices.addAll(devices.items);
|
||||
devices.items.forEach(device -> {
|
||||
ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, device.deviceFamily);
|
||||
if (SUPPORTED_DISCOVERY_THING_TYPES_UIDS.contains(thingTypeUID)) {
|
||||
for (Thing thing : getThing().getThings()) {
|
||||
ThingHandler handler = thing.getHandler();
|
||||
if (handler != null && ((MyQDeviceHandler) handler).getSerialNumber()
|
||||
.equalsIgnoreCase(device.serialNumber)) {
|
||||
((MyQDeviceHandler) handler).handleDeviceUpdate(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
devicesCache = currentDevices;
|
||||
}
|
||||
|
||||
private synchronized ContentResponse sendRequest(String url, HttpMethod method, @Nullable ContentProvider content,
|
||||
@Nullable String contentType)
|
||||
throws InterruptedException, MyQCommunicationException, MyQAuthenticationException {
|
||||
AccessTokenResponse tokenResponse = null;
|
||||
// if we don't need to force a login, attempt to use the token we have
|
||||
if (!needsLogin) {
|
||||
try {
|
||||
tokenResponse = getOAuthService().getAccessTokenResponse();
|
||||
} catch (OAuthException | IOException | OAuthResponseException e) {
|
||||
// ignore error, will try to login below
|
||||
logger.debug("Error accessing token, will attempt to login again", e);
|
||||
}
|
||||
}
|
||||
|
||||
// if no token, or we need to login, do so now
|
||||
if (tokenResponse == null) {
|
||||
tokenResponse = login();
|
||||
needsLogin = false;
|
||||
}
|
||||
|
||||
Request request = httpClient.newRequest(url).method(method).agent(userAgent)
|
||||
.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.header("Authorization", authTokenHeader(tokenResponse));
|
||||
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<ContentResponse> futureResult = new CompletableFuture<>();
|
||||
request.send(new BufferingResponseListener() {
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public void onComplete(Result result) {
|
||||
Response response = result.getResponse();
|
||||
futureResult.complete(new HttpContentResponse(response, getContent(), getMediaType(), getEncoding()));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
ContentResponse result = futureResult.get();
|
||||
logger.trace("Account Response - status: {} content: {}", result.getStatus(), result.getContentAsString());
|
||||
return result;
|
||||
} catch (ExecutionException e) {
|
||||
throw new MyQCommunicationException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T parseResultAndUpdateStatus(ContentResponse response, Gson parser, Class<T> classOfT)
|
||||
throws MyQCommunicationException {
|
||||
if (HttpStatus.isSuccess(response.getStatus())) {
|
||||
try {
|
||||
T responseObject = parser.fromJson(response.getContentAsString(), classOfT);
|
||||
if (responseObject != null) {
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
return responseObject;
|
||||
} else {
|
||||
throw new MyQCommunicationException("Bad response from server");
|
||||
}
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new MyQCommunicationException("Invalid JSON Response " + response.getContentAsString());
|
||||
}
|
||||
} else if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
|
||||
// our tokens no longer work, will need to login again
|
||||
needsLogin = true;
|
||||
throw new MyQCommunicationException("Token was rejected for request");
|
||||
} else {
|
||||
throw new MyQCommunicationException(
|
||||
"Invalid Response Code " + response.getStatus() + " : " + response.getContentAsString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MyQ login page which contains form elements and cookies needed to login
|
||||
*
|
||||
* @param codeVerifier
|
||||
* @return
|
||||
* @throws InterruptedException
|
||||
* @throws ExecutionException
|
||||
* @throws TimeoutException
|
||||
*/
|
||||
private ContentResponse getLoginPage(String codeVerifier)
|
||||
throws InterruptedException, ExecutionException, TimeoutException {
|
||||
try {
|
||||
Request request = httpClient.newRequest(LOGIN_AUTHORIZE_URL) //
|
||||
.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS) //
|
||||
.param("client_id", CLIENT_ID) //
|
||||
.param("code_challenge", generateCodeChallange(codeVerifier)) //
|
||||
.param("code_challenge_method", "S256") //
|
||||
.param("redirect_uri", REDIRECT_URI) //
|
||||
.param("response_type", "code") //
|
||||
.param("scope", SCOPE) //
|
||||
.agent(userAgent).followRedirects(true);
|
||||
request.header("Accept", "\"*/*\"");
|
||||
request.header("Authorization",
|
||||
"Basic " + Base64.getEncoder().encodeToString((CLIENT_ID + ":").getBytes()));
|
||||
logger.debug("Sending {} to {}", request.getMethod(), request.getURI());
|
||||
ContentResponse response = request.send();
|
||||
logger.debug("Login Code {} Response {}", response.getStatus(), response.getContentAsString());
|
||||
return response;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new ExecutionException(e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends configured credentials and elements from the login page in order to obtain a redirect location header value
|
||||
*
|
||||
* @param url
|
||||
* @param requestToken
|
||||
* @param returnURL
|
||||
* @return The location header value
|
||||
* @throws InterruptedException
|
||||
* @throws ExecutionException
|
||||
* @throws TimeoutException
|
||||
*/
|
||||
@Nullable
|
||||
private String postLogin(String url, String requestToken, String returnURL)
|
||||
throws InterruptedException, ExecutionException, TimeoutException {
|
||||
/*
|
||||
* on a successful post to this page we will get several redirects, and a final 301 to:
|
||||
* com.myqops://ios?code=0123456789&scope=MyQ_Residential%20offline_access&iss=https%3A%2F%2Fpartner-identity.
|
||||
* myq-cloud.com
|
||||
*
|
||||
* We can then take the parameters out of this location and continue the process
|
||||
*/
|
||||
Fields fields = new Fields();
|
||||
fields.add("Email", username);
|
||||
fields.add("Password", password);
|
||||
fields.add("__RequestVerificationToken", requestToken);
|
||||
fields.add("ReturnUrl", returnURL);
|
||||
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.POST) //
|
||||
.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS) //
|
||||
.content(new FormContentProvider(fields)) //
|
||||
.agent(userAgent) //
|
||||
.followRedirects(false);
|
||||
setCookies(request);
|
||||
|
||||
logger.debug("Posting Login to {}", url);
|
||||
ContentResponse response = request.send();
|
||||
|
||||
String location = null;
|
||||
|
||||
// follow redirects until we match our REDIRECT_URI or hit a redirect safety limit
|
||||
for (int i = 0; i < LOGIN_MAX_REDIRECTS && HttpStatus.isRedirection(response.getStatus()); i++) {
|
||||
|
||||
String loc = response.getHeaders().get("location");
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Redirect Login: Code {} Location Header: {} Response {}", response.getStatus(), loc,
|
||||
response.getContentAsString());
|
||||
}
|
||||
if (loc == null) {
|
||||
logger.debug("No location value");
|
||||
break;
|
||||
}
|
||||
if (loc.indexOf(REDIRECT_URI) == 0) {
|
||||
location = loc;
|
||||
break;
|
||||
}
|
||||
request = httpClient.newRequest(LOGIN_BASE_URL + loc).timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.agent(userAgent).followRedirects(false);
|
||||
setCookies(request);
|
||||
response = request.send();
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Final step of the login process to get an oAuth access response token
|
||||
*
|
||||
* @param redirectLocation
|
||||
* @param codeVerifier
|
||||
* @return
|
||||
* @throws InterruptedException
|
||||
* @throws ExecutionException
|
||||
* @throws TimeoutException
|
||||
*/
|
||||
private ContentResponse getLoginToken(String redirectLocation, String codeVerifier)
|
||||
throws InterruptedException, ExecutionException, TimeoutException {
|
||||
try {
|
||||
Map<String, String> params = parseLocationQuery(redirectLocation);
|
||||
|
||||
Fields fields = new Fields();
|
||||
fields.add("client_id", CLIENT_ID);
|
||||
fields.add("client_secret", Base64.getEncoder().encodeToString(CLIENT_SECRET.getBytes()));
|
||||
fields.add("code", params.get("code"));
|
||||
fields.add("code_verifier", codeVerifier);
|
||||
fields.add("grant_type", "authorization_code");
|
||||
fields.add("redirect_uri", REDIRECT_URI);
|
||||
fields.add("scope", params.get("scope"));
|
||||
|
||||
Request request = httpClient.newRequest(LOGIN_TOKEN_URL) //
|
||||
.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS) //
|
||||
.content(new FormContentProvider(fields)) //
|
||||
.method(HttpMethod.POST) //
|
||||
.agent(userAgent).followRedirects(true);
|
||||
setCookies(request);
|
||||
ContentResponse response = request.send();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Login Code {} Response {}", response.getStatus(), response.getContentAsString());
|
||||
}
|
||||
return response;
|
||||
} catch (URISyntaxException e) {
|
||||
throw new ExecutionException(e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
private OAuthClientService getOAuthService() {
|
||||
OAuthClientService oAuthService = this.oAuthService;
|
||||
if (oAuthService == null || oAuthService.isClosed()) {
|
||||
oAuthService = oAuthFactory.createOAuthClientService(getThing().toString(), LOGIN_TOKEN_URL,
|
||||
LOGIN_AUTHORIZE_URL, CLIENT_ID, CLIENT_SECRET, SCOPE, false);
|
||||
oAuthService.addAccessTokenRefreshListener(this);
|
||||
this.oAuthService = oAuthService;
|
||||
}
|
||||
return oAuthService;
|
||||
}
|
||||
|
||||
private String generateCodeVerifier() {
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
byte[] codeVerifier = new byte[32];
|
||||
secureRandom.nextBytes(codeVerifier);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifier);
|
||||
}
|
||||
|
||||
private String generateCodeChallange(String codeVerifier) throws NoSuchAlgorithmException {
|
||||
byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
messageDigest.update(bytes, 0, bytes.length);
|
||||
byte[] digest = messageDigest.digest();
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
}
|
||||
|
||||
private Map<String, String> parseLocationQuery(String location) throws URISyntaxException {
|
||||
URI uri = new URI(location);
|
||||
return Arrays.stream(uri.getQuery().split("&")).map(str -> str.split("="))
|
||||
.collect(Collectors.toMap(str -> str[0], str -> str[1]));
|
||||
}
|
||||
|
||||
private void setCookies(Request request) {
|
||||
for (HttpCookie c : httpClient.getCookieStore().getCookies()) {
|
||||
request.cookie(c);
|
||||
}
|
||||
}
|
||||
|
||||
private String authTokenHeader(AccessTokenResponse tokenResponse) {
|
||||
return tokenResponse.getTokenType() + " " + tokenResponse.getAccessToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception for authenticated related errors
|
||||
*/
|
||||
class MyQAuthenticationException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MyQAuthenticationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic exception for non authentication related errors when communicating with the MyQ service.
|
||||
*/
|
||||
class MyQCommunicationException extends IOException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MyQCommunicationException(@Nullable String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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 {
|
||||
void handleDeviceUpdate(DeviceDTO device);
|
||||
|
||||
String getSerialNumber();
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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 device;
|
||||
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 localDevice = device;
|
||||
if (bridge != null && localDevice != 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 percentage) {
|
||||
cmd = percentage.as(UpDownType.class) == UpDownType.UP ? "open" : "close";
|
||||
}
|
||||
if (command instanceof StringType) {
|
||||
cmd = command.toString();
|
||||
}
|
||||
if (cmd != null) {
|
||||
((MyQAccountHandler) handler).sendDoorAction(localDevice, cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSerialNumber() {
|
||||
return serialNumber;
|
||||
}
|
||||
|
||||
protected void updateState() {
|
||||
final DeviceDTO localDevice = device;
|
||||
if (localDevice != null) {
|
||||
String doorState = localDevice.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;
|
||||
}
|
||||
updateState("closeerror", localDevice.state.isUnattendedCloseAllowed ? OnOffType.OFF : OnOffType.ON);
|
||||
updateState("openerror", localDevice.state.isUnattendedOpenAllowed ? OnOffType.OFF : OnOffType.ON);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDeviceUpdate(DeviceDTO device) {
|
||||
if (!MyQBindingConstants.THING_TYPE_GARAGEDOOR.getId().equals(device.deviceFamily)) {
|
||||
return;
|
||||
}
|
||||
this.device = device;
|
||||
if (device.state.online) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
updateState();
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device reports as offline");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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 device;
|
||||
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 localDevice = device;
|
||||
if (bridge != null && localDevice != null) {
|
||||
BridgeHandler handler = bridge.getHandler();
|
||||
if (handler != null) {
|
||||
((MyQAccountHandler) handler).sendLampAction(localDevice, command == OnOffType.ON ? "on" : "off");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSerialNumber() {
|
||||
return serialNumber;
|
||||
}
|
||||
|
||||
protected void updateState() {
|
||||
final DeviceDTO localDevice = device;
|
||||
if (localDevice != null) {
|
||||
String lampState = localDevice.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;
|
||||
}
|
||||
this.device = device;
|
||||
if (device.state.online) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
updateState();
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device reports as offline");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="myq" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>MyQ Binding</name>
|
||||
<description>The MyQ binding allows monitoring and control of garage doors that are MyQ enabled.</description>
|
||||
<connection>cloud</connection>
|
||||
|
||||
</addon:addon>
|
|
@ -1,37 +0,0 @@
|
|||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:myq:account">
|
||||
<parameter name="username" type="text" required="true">
|
||||
<label>User Name</label>
|
||||
<description>Account username</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="true">
|
||||
<label>password</label>
|
||||
<description>Account password</description>
|
||||
<context>password</context>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" min="30" required="true" unit="s">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Specifies the refresh interval in seconds</description>
|
||||
<default>60</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="thing-type:myq:garagedoor">
|
||||
<parameter name="serialNumber" type="text" required="true">
|
||||
<label>Serial Number</label>
|
||||
<description>Serial number of the garage door</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="thing-type:myq:lamp">
|
||||
<parameter name="serialNumber" type="text" required="true">
|
||||
<label>Serial Number</label>
|
||||
<description>Serial number of the lamp</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
|
@ -1,42 +0,0 @@
|
|||
# add-on
|
||||
|
||||
addon.myq.name = MyQ Binding
|
||||
addon.myq.description = The MyQ binding allows monitoring and control of garage doors that are MyQ enabled.
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.myq.account.label = MyQ Account
|
||||
thing-type.myq.account.description = MyQ Cloud Account
|
||||
thing-type.myq.garagedoor.label = MyQ Garage Door
|
||||
thing-type.myq.garagedoor.description = MyQ Garage Door
|
||||
thing-type.myq.lamp.label = MyQ Lamp
|
||||
thing-type.myq.lamp.description = MyQ Lamp
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.myq.account.password.label = password
|
||||
thing-type.config.myq.account.password.description = Account password
|
||||
thing-type.config.myq.account.refreshInterval.label = Refresh Interval
|
||||
thing-type.config.myq.account.refreshInterval.description = Specifies the refresh interval in seconds
|
||||
thing-type.config.myq.account.username.label = User Name
|
||||
thing-type.config.myq.account.username.description = Account username
|
||||
thing-type.config.myq.garagedoor.serialNumber.label = Serial Number
|
||||
thing-type.config.myq.garagedoor.serialNumber.description = Serial number of the garage door
|
||||
thing-type.config.myq.lamp.serialNumber.label = Serial Number
|
||||
thing-type.config.myq.lamp.serialNumber.description = Serial number of the lamp
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.myq.doorcloseerror.label = Garage Door Close Error
|
||||
channel-type.myq.dooropenerror.label = Garage Door Open Error
|
||||
channel-type.myq.doorrollershutter.label = Garage Door Rollershutter
|
||||
channel-type.myq.doorstatus.label = Garage Door Status
|
||||
channel-type.myq.doorstatus.state.option.open = Open
|
||||
channel-type.myq.doorstatus.state.option.opening = Opening
|
||||
channel-type.myq.doorstatus.state.option.closed = Closed
|
||||
channel-type.myq.doorstatus.state.option.closing = Closing
|
||||
channel-type.myq.doorstatus.state.option.stopped = Stopped
|
||||
channel-type.myq.doorstatus.state.option.transition = Transitioning
|
||||
channel-type.myq.doorstatus.state.option.unknown = Unknown
|
||||
channel-type.myq.doorswitch.label = Garage Door Switch
|
||||
channel-type.myq.lampswitch.label = Lamp Switch
|
|
@ -1,79 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="myq" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="account">
|
||||
<label>MyQ Account</label>
|
||||
<description>MyQ Cloud Account</description>
|
||||
<config-description-ref uri="thing-type:myq:account"/>
|
||||
</bridge-type>
|
||||
|
||||
<thing-type id="garagedoor">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="account"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>MyQ Garage Door</label>
|
||||
<description>MyQ Garage Door</description>
|
||||
<channels>
|
||||
<channel id="status" typeId="doorstatus"/>
|
||||
<channel id="switch" typeId="doorswitch"/>
|
||||
<channel id="rollershutter" typeId="doorrollershutter"/>
|
||||
<channel id="closeerror" typeId="doorcloseerror"/>
|
||||
<channel id="openerror" typeId="dooropenerror"/>
|
||||
</channels>
|
||||
<representation-property>serialNumber</representation-property>
|
||||
<config-description-ref uri="thing-type:myq:garagedoor"/>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="lamp">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="account"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>MyQ Lamp</label>
|
||||
<description>MyQ Lamp</description>
|
||||
<channels>
|
||||
<channel id="switch" typeId="lampswitch"/>
|
||||
</channels>
|
||||
<representation-property>serialNumber</representation-property>
|
||||
<config-description-ref uri="thing-type:myq:lamp"/>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="doorstatus">
|
||||
<item-type>String</item-type>
|
||||
<label>Garage Door Status</label>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="open">Open</option>
|
||||
<option value="opening">Opening</option>
|
||||
<option value="closed">Closed</option>
|
||||
<option value="closing">Closing</option>
|
||||
<option value="stopped">Stopped</option>
|
||||
<option value="transition">Transitioning</option>
|
||||
<option value="unknown">Unknown</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="doorswitch">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Garage Door Switch</label>
|
||||
</channel-type>
|
||||
<channel-type id="doorrollershutter">
|
||||
<item-type>Rollershutter</item-type>
|
||||
<label>Garage Door Rollershutter</label>
|
||||
</channel-type>
|
||||
<channel-type id="doorcloseerror">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Garage Door Close Error</label>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="dooropenerror">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Garage Door Open Error</label>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="lampswitch">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Lamp Switch</label>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
|
@ -258,7 +258,6 @@
|
|||
<module>org.openhab.binding.mybmw</module>
|
||||
<module>org.openhab.binding.mycroft</module>
|
||||
<module>org.openhab.binding.mynice</module>
|
||||
<module>org.openhab.binding.myq</module>
|
||||
<module>org.openhab.binding.mystrom</module>
|
||||
<module>org.openhab.binding.nanoleaf</module>
|
||||
<module>org.openhab.binding.neato</module>
|
||||
|
|
Loading…
Reference in New Issue