mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[nuki] Opener support and discovery (#10672)
* [nuki] Opener support and discovery (#10671) * Added Opener support * New Smartlock channels * Discovery support * Secured communication using hashed token * Improved callback handling Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Fixed code style problems Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Id of bridge is unique for openhab instance Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Added compatibility with smart lock created by previous binding version Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Secure token configuration + threading improvements Added configuration option to enable or disable using hashed token for bridge API authentication. Turning bridge offline when device request fails. All HTTP calls made async so that openhab thread is not blocked. Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Only discover bridges which return some response Only bridges which return 200 or 403 response are discovered. Nuki API might return bridges which no longer exists or are on different network and are not reachable. We do not want to put these in inbox, only those who respond do HTTP calls. Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Changed ownership of nuki binding Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Code review changes * Fixed @Nullable annotations * Fixed too many logger messages * Rewritten configuration to use configuration class Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Code review changes * NukiId moved back to configuration and switched to configuration classes in all devices * Doorbell ringing channel switched to trigger channel Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Code review changes * Channel description reformatted into table * Fixed bug where repeated bridge discovery was overwriting existing bridge properties Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Fixed ringactionTimestamp property name Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Fixed search of changed thing in HTTP callback Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Improvements to uuid generation * Bridge id is generated only from bridgeId property, making it stable across openhab installations * Using InstanceUUID as unique callback identifier instead of bridge id Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Improved duplicate thing discovery * Implemented duplicate thing detection using openHAB ThingRegistry Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com> * Code review changes * Fixed all nullable warnings * Fixed README formatting Signed-off-by: Jan Vybíral <jan.vybiral1@gmail.com>
This commit is contained in:
parent
b37022c5d7
commit
d1171ca809
@ -205,7 +205,7 @@
|
||||
/bundles/org.openhab.binding.nikohomecontrol/ @mherwege
|
||||
/bundles/org.openhab.binding.novafinedust/ @t2000
|
||||
/bundles/org.openhab.binding.ntp/ @marcelrv
|
||||
/bundles/org.openhab.binding.nuki/ @mkatter
|
||||
/bundles/org.openhab.binding.nuki/ @janvyb
|
||||
/bundles/org.openhab.binding.nuvo/ @mlobstein
|
||||
/bundles/org.openhab.binding.nzwateralerts/ @cossey
|
||||
/bundles/org.openhab.binding.oceanic/ @kgoderis
|
||||
|
@ -1,15 +1,14 @@
|
||||
# Nuki Binding
|
||||
|
||||
This is the binding for the [Nuki Smart Lock](https://nuki.io).
|
||||
This binding allows you to integrate, view, control and configure the Nuki Bridge and Nuki Smart Locks.
|
||||
This binding allows you to integrate, view, control and configure the Nuki Bridge, Nuki Smart Lock and Nuki Opener.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. At least one Nuki Smart Lock which is paired via Bluetooth with a Nuki Bridge. For this go and get either:
|
||||
* a [Nuki Smart Lock](https://nuki.io/en/smart-lock/) and a [Nuki Bridge](https://nuki.io/en/bridge/) or
|
||||
* the [Nuki Combo](https://nuki.io/en/shop/nuki-combo/) or
|
||||
* a [Nuki Smart Lock](https://nuki.io/en/smart-lock/) and the Nuki [Nuki Software Bridge](https://play.google.com/store/apps/details?id=io.nuki.bridge)
|
||||
2. The Bridge HTTP-API has to be enabled during [Initial Bridge setup](https://nuki.io/en/support/bridge/bridge-setup/initial-bridge-setup/). Note down the IP, Port and API token.
|
||||
1. At least one Nuki Smart Lock or Nuki Opener which is paired via Bluetooth with a Nuki Bridge. For this go and get either:
|
||||
* [Nuki Smart Lock](https://nuki.io/en/smart-lock/) and a [Nuki Bridge](https://nuki.io/en/bridge/)
|
||||
* [Nuki Combo](https://nuki.io/en/shop/nuki-combo/)
|
||||
2. The Bridge HTTP-API has to be enabled during [Initial Bridge setup](https://nuki.io/en/support/bridge/bridge-setup/initial-bridge-setup/).
|
||||
|
||||
It is absolutely recommended to configure static IP addresses for both, the openHAB server and the Nuki Bridge!
|
||||
|
||||
@ -24,7 +23,7 @@ The Sheet [NukiBridgeAPI](https://docs.google.com/spreadsheets/d/1SGKWhqwqRyOGbv
|
||||
|
||||
## Supported Bridges
|
||||
|
||||
This binding supports just one bridge type: The Nuki Bridge. Create one `bridge` per Nuki Bridge available in your home automation environment.
|
||||
This binding supports just one bridge type: The Nuki Bridge (`nuki:bridge`). Create one `bridge` per Nuki Bridge available in your home automation environment.
|
||||
|
||||
The following configuration options are available:
|
||||
|
||||
@ -34,35 +33,139 @@ The following configuration options are available:
|
||||
| port | The Port which you configured during [Initial Bridge setup](https://nuki.io/en/support/bridge/bridge-setup/initial-bridge-setup/). | Default 8080 |
|
||||
| apiToken | The API Token which you configured during [Initial Bridge setup](https://nuki.io/en/support/bridge/bridge-setup/initial-bridge-setup/). | Required |
|
||||
| manageCallbacks | Let the Nuki Binding manage the callbacks on the Nuki Bridge. It will add the required callback on the Nuki Bridge. If there are already 3 callbacks, it **will delete** the callback with ID `0`. | Default true |
|
||||
| secureToken | Whether hashed token should be used when communicating with Nuki Bridge. If disabled, API token will be sent in plaintext with each request. | Default true |
|
||||
|
||||
### Bridge discovery
|
||||
|
||||
Bridges on local network can be discovered automatically if both Nuki Bridge and openHAB have working internet connection. You can check whether discovery
|
||||
is working by checking [discovery API endpoint](https://api.nuki.io/discover/bridges). To discover bridges do the following:
|
||||
|
||||
* In openHAB UI add new thing, select Nuki Binding and start scan. LED on bridge should light up.
|
||||
* Within 30s press button on Nuki Bridge you want to discover.
|
||||
* Bridge should appear in inbox.
|
||||
|
||||
Pressing bridge button is required for binding to obtain valid API token. If the button isn't pressed during discovery, bridge will
|
||||
be created but token must be set manually for binding to work.
|
||||
|
||||
If bridge is connected to network but not discovered, enter [Manage Bridge](https://support.nuki.io/hc/en-us/articles/360016489018-Manage-Bridge) menu
|
||||
in Nuki mobile app, check server connection then disconnect and let the bridge restart.
|
||||
|
||||
## Supported Things
|
||||
|
||||
This binding support just one thing type: The Nuki Smart Lock. Create one `smartlock` per Nuki Smart Lock available in you home automation environment.
|
||||
This binding supports 2 things - Nuki Smart Lock (`nuki:smartlock`) and Nuki Opener (`nuki:opener`). Both devices can be added using discovery after bridge they are
|
||||
connected to is configured and online.
|
||||
|
||||
### Nuki Smart Lock
|
||||
|
||||
The following configuration options are available:
|
||||
|
||||
| Parameter | Description | Comment |
|
||||
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| nukiId | The `Nuki-ID` of the Nuki Smart Lock. It is a 8-digit hexadecimal string. Look it up on the sticker on the back of the Nuki Smart Lock (remove mounting plate). | Required |
|
||||
| unlatch | If set to `true` the Nuki Smart Lock will unlock the door but then also automatically pull the latch of the door lock. Usually, if the door hinges are correctly adjusted, the door will then swing open. | Default false |
|
||||
| Parameter | Description | Comment |
|
||||
|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
|
||||
| unlatch | If set to `true` the Nuki Smart Lock will unlock the door but then also automatically pull the latch of the door lock. Usually, if the door hinges are correctly adjusted, the door will then swing open. | Default false |
|
||||
|
||||
## Supported Channels
|
||||
#### Supported Channels
|
||||
|
||||
- **lock** (Switch)
|
||||
Use this channel with a Switch Item to lock and unlock the door.
|
||||
| Channel | Type | Description |
|
||||
|------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| lock | Switch | Switch to lock and unlock doors. If `unlatch` configuration parameter is set, unlocking will also unlatch the door. |
|
||||
| lockState | Number | Channel which accepts [Supported commands](#supported-lockstate-commands) for performing actions, and produces [supported values](#supported-lockstate-values) when lock state changes. |
|
||||
| lowBattery | Switch | Low battery warning channel |
|
||||
| keypadLowBattery | Switch | Indicates if keypad connected to Nuki Lock has low battery |
|
||||
| batteryLevel | Number | Current battery level |
|
||||
| batteryCharging | Swtich | Flag indicating if the batteries of the Nuki device are charging at the moment |
|
||||
| doorsensorState | Number | Read only channel for monitoring door sensor state, see [supported values](#supported-doorsensorstate-values) |
|
||||
|
||||
- **lockState** (Number)
|
||||
Use this channel if you want to execute other supported lock actions or to display the current lock state.
|
||||
Supported Lock Actions are: `2` (Unlock), `7` (Unlatch), `1002` (Lock 'n' Go), `1007` (Lock 'n' Go with Unlatch) and `4` (Lock).
|
||||
Supported Lock States are : `1` (Locked), `2` (Unlocking), `3` (Unlocked), `4` (Locking), `7` (Unlatching), `1002` (Unlocking initiated through Lock 'n' Go) and `1007` (Unlatching initiated through Lock 'n' Go with Unlatch).
|
||||
Unfortunately the Nuki Bridge is not reporting any transition states (e.g. for Lock 'n' Go).
|
||||
##### Supported lockState commands
|
||||
|
||||
- **lowBattery** (Switch)
|
||||
Use this channel to receive a low battery warning.
|
||||
These values can be sent to _lockState_ channel as a commands:
|
||||
|
||||
- **doorsensorState** (Number)
|
||||
Use this channel if you want to display the current door state provided by the door sensor.
|
||||
Supported Door Sensor States are : `0` (Unavailable), `1` (Deactivated), `2` (Closed), `3` (Open), `4` (Unknown) and `5` (Calibrating).
|
||||
| Command | Name |
|
||||
|---------|--------------------------|
|
||||
| 1 | Unlock |
|
||||
| 2 | Lock |
|
||||
| 3 | Unlatch |
|
||||
| 4 | Lock 'n' Go |
|
||||
| 5 | Lock 'n' Go with Unlatch |
|
||||
|
||||
##### Supported lockState values
|
||||
|
||||
| State | Name |
|
||||
|--------|--------------------------|
|
||||
| 0 | Uncalibrated |
|
||||
| 1 | Locked |
|
||||
| 2 | Unlocking |
|
||||
| 3 | Unlocked |
|
||||
| 4 | Locking |
|
||||
| 5 | Unlatched |
|
||||
| 6 | Unlatched (Lock 'n' Go) |
|
||||
| 7 | Unlatching |
|
||||
| 254 | Motor blocked |
|
||||
| 255 | Undefined |
|
||||
|
||||
Unfortunately the Nuki Bridge is not reporting any transition states (e.g. for Lock 'n' Go).
|
||||
|
||||
##### Supported doorSensorState values
|
||||
|
||||
| State | Name |
|
||||
|--------|--------------------------|
|
||||
| 0 | Unavailable |
|
||||
| 1 | Deactivated |
|
||||
| 2 | Closed |
|
||||
| 3 | Open |
|
||||
| 4 | Unknown |
|
||||
| 5 | Calibrating |
|
||||
|
||||
### Nuki Opener
|
||||
|
||||
Nuki Opener has no configuration properties.
|
||||
|
||||
#### Supported channels
|
||||
|
||||
| Channel | Type | Description |
|
||||
|---------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| openerState | Number | Channel for sending [supported commands](#supported-openerstate-commands) to Opener, produces one of [supported values](#supported-openerstate-values) when Opener state changes |
|
||||
| openerMode | Number | Id of current Opener mode, see [Supported values](#supported-openermode-values) |
|
||||
| openerLowBattery | Switch | Low battery warning channel |
|
||||
| ringActionState | Trigger | Channel triggers 'RINGING' event when the doorbell is being rung. This can trigger at most once every 30s |
|
||||
| ringActionTimestamp | DateTime | Timestamp of last time doorbell was rung. |
|
||||
|
||||
##### Supported openerState commands
|
||||
|
||||
| Command | Name |
|
||||
|---------|----------------------------|
|
||||
| 1 | Activate ring to open |
|
||||
| 2 | Deactivate ring to open |
|
||||
| 3 | Electric strike actuation |
|
||||
| 4 | Activate continuous mode |
|
||||
| 5 | Deactivate continuous mode |
|
||||
|
||||
##### Supported openerState values
|
||||
|
||||
| State | Name |
|
||||
|--------|---------------------|
|
||||
| 0 | Untrained |
|
||||
| 1 | Online |
|
||||
| 3 | Ring to open active |
|
||||
| 5 | Open |
|
||||
| 7 | Opening |
|
||||
| 253 | Boot run |
|
||||
| 255 | Undefined |
|
||||
|
||||
##### Supported openerMode values
|
||||
|
||||
| Mode | Name |
|
||||
|--------|-----------------|
|
||||
| 2 | Door mode |
|
||||
| 3 | Continuous mode |
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bridge and devices are offline with error 403
|
||||
|
||||
If secureToken property is enabled, make sure that time on device running openHAB and Nuki Bridge are synchronized. When secureToken
|
||||
is enabled, all requests contain timestamp and bridge will only accept requests with small time difference. If it is not possible to
|
||||
keep time synchronized, disable secureToken feature.
|
||||
|
||||
## Full Example
|
||||
|
||||
|
@ -15,15 +15,21 @@ package org.openhab.binding.nuki.internal;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiLinkBuilder;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.NukiApiServlet;
|
||||
import org.openhab.binding.nuki.internal.handler.NukiBridgeHandler;
|
||||
import org.openhab.binding.nuki.internal.handler.NukiOpenerHandler;
|
||||
import org.openhab.binding.nuki.internal.handler.NukiSmartLockHandler;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.id.InstanceUUID;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.net.HttpServiceUtil;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
@ -39,6 +45,7 @@ import org.slf4j.LoggerFactory;
|
||||
* handlers.
|
||||
*
|
||||
* @author Markus Katter - Initial contribution
|
||||
* @contributer Jan Vybíral - Improved thing id generation
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.nuki")
|
||||
@NonNullByDefault
|
||||
@ -46,18 +53,16 @@ public class NukiHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NukiHandlerFactory.class);
|
||||
|
||||
private final HttpService httpService;
|
||||
private final HttpClient httpClient;
|
||||
private final NetworkAddressService networkAddressService;
|
||||
private @Nullable String callbackUrl;
|
||||
private @Nullable NukiApiServlet nukiApiServlet;
|
||||
private NukiApiServlet nukiApiServlet;
|
||||
|
||||
@Activate
|
||||
public NukiHandlerFactory(@Reference HttpService httpService, @Reference final HttpClientFactory httpClientFactory,
|
||||
@Reference NetworkAddressService networkAddressService) {
|
||||
this.httpService = httpService;
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
this.networkAddressService = networkAddressService;
|
||||
this.nukiApiServlet = new NukiApiServlet(httpService);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -67,47 +72,45 @@ public class NukiHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
logger.debug("NukiHandlerFactory:createHandler({})", thing);
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (NukiBindingConstants.THING_TYPE_BRIDGE_UIDS.contains(thingTypeUID)) {
|
||||
callbackUrl = createCallbackUrl();
|
||||
String callbackUrl = createCallbackUrl(InstanceUUID.get());
|
||||
NukiBridgeHandler nukiBridgeHandler = new NukiBridgeHandler((Bridge) thing, httpClient, callbackUrl);
|
||||
if (!nukiBridgeHandler.isInitializable()) {
|
||||
return null;
|
||||
}
|
||||
if (nukiApiServlet == null) {
|
||||
nukiApiServlet = new NukiApiServlet(httpService);
|
||||
}
|
||||
nukiApiServlet.add(nukiBridgeHandler);
|
||||
return nukiBridgeHandler;
|
||||
} else if (NukiBindingConstants.THING_TYPE_SMARTLOCK_UIDS.contains(thingTypeUID)) {
|
||||
return new NukiSmartLockHandler(thing);
|
||||
} else if (NukiBindingConstants.THING_TYPE_OPENER_UIDS.contains(thingTypeUID)) {
|
||||
return new NukiOpenerHandler(thing);
|
||||
}
|
||||
logger.trace("No valid Handler found for Thing[{}]!", thingTypeUID);
|
||||
logger.warn("No valid Handler found for Thing[{}]!", thingTypeUID);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration, ThingUID thingUID) {
|
||||
return super.createThing(thingTypeUID, configuration, thingUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeThing(ThingUID thingUID) {
|
||||
super.removeThing(thingUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterHandler(Thing thing) {
|
||||
super.unregisterHandler(thing);
|
||||
logger.trace("NukiHandlerFactory:unregisterHandler({})", thing);
|
||||
if (thing.getHandler() instanceof NukiBridgeHandler && nukiApiServlet != null) {
|
||||
nukiApiServlet.remove((NukiBridgeHandler) thing.getHandler());
|
||||
if (nukiApiServlet.countNukiBridgeHandlers() == 0) {
|
||||
nukiApiServlet = null;
|
||||
}
|
||||
ThingHandler handler = thing.getHandler();
|
||||
if (handler instanceof NukiBridgeHandler) {
|
||||
nukiApiServlet.remove((NukiBridgeHandler) handler);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable String createCallbackUrl() {
|
||||
logger.trace("createCallbackUrl()");
|
||||
if (callbackUrl != null) {
|
||||
return callbackUrl;
|
||||
}
|
||||
private @Nullable String createCallbackUrl(String id) {
|
||||
final String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
|
||||
if (ipAddress == null) {
|
||||
logger.warn("No network interface could be found.");
|
||||
logger.warn("No network interface could be found to get callback address");
|
||||
return null;
|
||||
}
|
||||
// we do not use SSL as it can cause certificate validation issues.
|
||||
@ -116,7 +119,7 @@ public class NukiHandlerFactory extends BaseThingHandlerFactory {
|
||||
logger.warn("Cannot find port of the http service.");
|
||||
return null;
|
||||
}
|
||||
String callbackUrl = String.format(NukiBindingConstants.CALLBACK_URL, ipAddress + ":" + port);
|
||||
String callbackUrl = NukiLinkBuilder.callbackUri(ipAddress, port, id).toString();
|
||||
logger.trace("callbackUrl[{}]", callbackUrl);
|
||||
return callbackUrl;
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.configuration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Configuration for Nuki Bridge
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NukiBridgeConfiguration {
|
||||
@Nullable
|
||||
public String ip;
|
||||
@Nullable
|
||||
public Integer port;
|
||||
@Nullable
|
||||
public String apiToken;
|
||||
public boolean manageCallbacks = true;
|
||||
public boolean secureToken = true;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.configuration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Configuration for Nuki devices
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NukiDeviceConfiguration {
|
||||
public String nukiId = "";
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.configuration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Configuration for Nuki Smart Lock
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NukiSmartLockConfiguration extends NukiDeviceConfiguration {
|
||||
public boolean unlatch = false;
|
||||
}
|
@ -10,13 +10,14 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal;
|
||||
package org.openhab.binding.nuki.internal.constants;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
@ -25,7 +26,9 @@ import org.openhab.core.thing.ThingTypeUID;
|
||||
*
|
||||
* @author Markus Katter - Initial contribution
|
||||
* @contributer Christian Hoefler - Door sensor integration
|
||||
* @contributer Jan Vybíral - Opener integration
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NukiBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "nuki";
|
||||
@ -33,38 +36,53 @@ public class NukiBindingConstants {
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
|
||||
public static final ThingTypeUID THING_TYPE_SMARTLOCK = new ThingTypeUID(BINDING_ID, "smartlock");
|
||||
public static final ThingTypeUID THING_TYPE_OPENER = new ThingTypeUID(BINDING_ID, "opener");
|
||||
|
||||
public static final Set<ThingTypeUID> THING_TYPE_BRIDGE_UIDS = Collections.singleton(THING_TYPE_BRIDGE);
|
||||
public static final Set<ThingTypeUID> THING_TYPE_SMARTLOCK_UIDS = Collections.singleton(THING_TYPE_SMARTLOCK);
|
||||
public static final Set<ThingTypeUID> THING_TYPE_OPENER_UIDS = Collections.singleton(THING_TYPE_OPENER);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
|
||||
.concat(THING_TYPE_BRIDGE_UIDS.stream(), THING_TYPE_SMARTLOCK_UIDS.stream()).collect(Collectors.toSet());
|
||||
.of(THING_TYPE_BRIDGE_UIDS, THING_TYPE_SMARTLOCK_UIDS, THING_TYPE_OPENER_UIDS).flatMap(Set::stream)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// List of all Channel ids
|
||||
// Device Types
|
||||
public static final int DEVICE_SMART_LOCK = 0;
|
||||
public static final int DEVICE_OPENER = 2;
|
||||
public static final Set<Integer> SUPPORTED_DEVICES = Set.of(DEVICE_OPENER, DEVICE_SMART_LOCK);
|
||||
|
||||
// Properties
|
||||
public static final String PROPERTY_WIFI_FIRMWARE_VERSION = "wifiFirmwareVersion";
|
||||
public static final String PROPERTY_HARDWARE_ID = "hardwareId";
|
||||
public static final String PROPERTY_SERVER_ID = "serverId";
|
||||
public static final String PROPERTY_FIRMWARE_VERSION = "firmwareVersion";
|
||||
public static final String PROPERTY_NAME = "name";
|
||||
public static final String PROPERTY_NUKI_ID = "nukiId";
|
||||
public static final String PROPERTY_BRIDGE_ID = "bridgeId";
|
||||
|
||||
// List of all Smart Lock Channel ids
|
||||
public static final String CHANNEL_SMARTLOCK_LOCK = "lock";
|
||||
public static final String CHANNEL_SMARTLOCK_STATE = "lockState";
|
||||
public static final String CHANNEL_SMARTLOCK_LOW_BATTERY = "lowBattery";
|
||||
public static final String CHANNEL_SMARTLOCK_KEYPAD_LOW_BATTERY = "keypadLowBattery";
|
||||
public static final String CHANNEL_SMARTLOCK_BATTERY_LEVEL = "batteryLevel";
|
||||
public static final String CHANNEL_SMARTLOCK_BATTERY_CHARGING = "batteryCharging";
|
||||
public static final String CHANNEL_SMARTLOCK_DOOR_STATE = "doorsensorState";
|
||||
|
||||
// List of all Opener Channel ids
|
||||
public static final String CHANNEL_OPENER_STATE = "openerState";
|
||||
public static final String CHANNEL_OPENER_MODE = "openerMode";
|
||||
public static final String CHANNEL_OPENER_LOW_BATTERY = "openerLowBattery";
|
||||
public static final String CHANNEL_OPENER_RING_ACTION_STATE = "ringActionState";
|
||||
public static final String CHANNEL_OPENER_RING_ACTION_TIMESTAMP = "ringActionTimestamp";
|
||||
|
||||
// List of all config-description parameters
|
||||
public static final String CONFIG_IP = "ip";
|
||||
public static final String CONFIG_PORT = "port";
|
||||
public static final String CONFIG_MANAGECB = "manageCallbacks";
|
||||
public static final String CONFIG_API_TOKEN = "apiToken";
|
||||
public static final String CONFIG_NUKI_ID = "nukiId";
|
||||
public static final String CONFIG_UNLATCH = "unlatch";
|
||||
|
||||
// Nuki Bridge API REST Endpoints
|
||||
public static final String URI_INFO = "http://%s:%s/info?token=%s";
|
||||
public static final String URI_LOCKSTATE = "http://%s:%s/lockState?token=%s&nukiId=%s";
|
||||
public static final String URI_LOCKACTION = "http://%s:%s/lockAction?token=%s&nukiId=%s&action=%s";
|
||||
public static final String URI_CBADD = "http://%s:%s/callback/add?token=%s&url=%s";
|
||||
public static final String URI_CBLIST = "http://%s:%s/callback/list?token=%s";
|
||||
public static final String URI_CBREMOVE = "http://%s:%s/callback/remove?token=%s&id=%s";
|
||||
|
||||
// openHAB Callback Endpoint & Nuki Bridge Callback URL
|
||||
public static final String CALLBACK_ENDPOINT = "/nuki/bcb";
|
||||
public static final String CALLBACK_URL = "http://%s" + CALLBACK_ENDPOINT;
|
||||
public static final String CONFIG_SECURE_TOKEN = "secureToken";
|
||||
|
||||
// Nuki Bridge API Lock Actions
|
||||
public static final int LOCK_ACTIONS_UNLOCK = 1;
|
||||
@ -96,4 +114,7 @@ public class NukiBindingConstants {
|
||||
public static final int DOORSENSOR_STATES_OPEN = 3;
|
||||
public static final int DOORSENSOR_STATES_UNKNOWN = 4;
|
||||
public static final int DOORSENSOR_STATES_CALIBRATING = 5;
|
||||
|
||||
// trigger channel events
|
||||
public static final String EVENT_RINGING = "RINGING";
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.constants;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
|
||||
/**
|
||||
* The {@link NukiLinkBuilder} class helps with constructing links to various Nuki APIs.
|
||||
* Links to secured APIs will be created with all necessary authentication parameters.
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NukiLinkBuilder {
|
||||
public static final URI URI_BRIDGE_DISCOVERY = URI.create("https://api.nuki.io/discover/bridges");
|
||||
public static final String CALLBACK_ENDPOINT = "/nuki/bcb";
|
||||
|
||||
private static final String PATH_INFO = "/info";
|
||||
private static final String PATH_AUTH = "/auth";
|
||||
private static final String PATH_LOCKSTATE = "/lockState";
|
||||
private static final String PATH_LOCKACTION = "/lockAction";
|
||||
public static final String PATH_CBADD = "/callback/add";
|
||||
public static final String PATH_CBLIST = "/callback/list";
|
||||
public static final String PATH_CBREMOVE = "/callback/remove";
|
||||
public static final String PATH_LIST = "/list";
|
||||
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final String token;
|
||||
private final boolean secureToken;
|
||||
private final SecureRandom random = new SecureRandom();
|
||||
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX");
|
||||
|
||||
/**
|
||||
* Create new instance of link builder
|
||||
*
|
||||
* @param host Hostname/ip address of Nuki bridge
|
||||
* @param port Port of Nuki bridge
|
||||
* @param token Token for authenticating API calls
|
||||
*/
|
||||
public NukiLinkBuilder(String host, int port, String token, boolean secureToken) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.token = token;
|
||||
this.secureToken = secureToken;
|
||||
}
|
||||
|
||||
public static URI getAuthUri(String host, int port) {
|
||||
return UriBuilder.fromPath(PATH_AUTH).host(host).port(port).scheme("http").build();
|
||||
}
|
||||
|
||||
private UriBuilder builder(String path) {
|
||||
return UriBuilder.fromPath(path).scheme("http").host(host).port(port);
|
||||
}
|
||||
|
||||
public URI info() {
|
||||
return buildWithAuth(builder(PATH_INFO));
|
||||
}
|
||||
|
||||
public URI lockState(String nukiId, int deviceType) {
|
||||
return buildWithAuth(builder(PATH_LOCKSTATE).queryParam("nukiId", nukiId).queryParam("deviceType", deviceType));
|
||||
}
|
||||
|
||||
public URI lockAction(String nukiId, int deviceType, int action) {
|
||||
return buildWithAuth(builder(PATH_LOCKACTION).queryParam("deviceType", deviceType).queryParam("action", action)
|
||||
.queryParam("nukiId", nukiId));
|
||||
}
|
||||
|
||||
public URI callbackList() {
|
||||
return buildWithAuth(builder(PATH_CBLIST));
|
||||
}
|
||||
|
||||
public URI callbackAdd(String callback) {
|
||||
String callbackEncoded = URLEncoder.encode(callback, StandardCharsets.UTF_8);
|
||||
return buildWithAuth(builder(PATH_CBADD).queryParam("url", callbackEncoded));
|
||||
}
|
||||
|
||||
public URI callbackRemove(int id) {
|
||||
return buildWithAuth(builder(PATH_CBREMOVE).queryParam("id", id));
|
||||
}
|
||||
|
||||
public URI list() {
|
||||
return buildWithAuth(builder(PATH_LIST));
|
||||
}
|
||||
|
||||
public static UriBuilder callbackPath(String callbackId) {
|
||||
return UriBuilder.fromPath(CALLBACK_ENDPOINT).queryParam("callbackId", callbackId);
|
||||
}
|
||||
|
||||
public static URI callbackUri(String host, int port, String callbackId) {
|
||||
return callbackPath(callbackId).host(host).port(port).scheme("http").build();
|
||||
}
|
||||
|
||||
private URI buildWithAuth(UriBuilder builder) {
|
||||
if (secureToken) {
|
||||
return buildWithHashedToken(builder);
|
||||
} else {
|
||||
return buildWithPlainToken(builder);
|
||||
}
|
||||
}
|
||||
|
||||
private URI buildWithHashedToken(UriBuilder builder) {
|
||||
String ts = formatter.format(OffsetDateTime.now(ZoneOffset.UTC));
|
||||
Integer rnr = random.nextInt(65536);
|
||||
String hashedToken = sha256(ts + "," + rnr + "," + token);
|
||||
|
||||
return builder.queryParam("ts", ts).queryParam("rnr", rnr).queryParam("hash", hashedToken).build();
|
||||
}
|
||||
|
||||
private URI buildWithPlainToken(UriBuilder builder) {
|
||||
return builder.queryParam("token", token).build();
|
||||
}
|
||||
|
||||
public static String sha256(String data) {
|
||||
MessageDigest digest;
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 Algorithm not supported", e);
|
||||
}
|
||||
byte[] rawHash = digest.digest(data.getBytes(StandardCharsets.UTF_8));
|
||||
return HexUtils.bytesToHex(rawHash).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.constants;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Enumeration of all lock actions Nuki Opener accepts
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum OpenerAction {
|
||||
ACTIVATE_RING_TO_OPEN(1),
|
||||
DEACTIVATE_RING_TO_OPEN(2),
|
||||
ELECTRIC_STRIKE_ACTUATION(3),
|
||||
ACTIVATE_CONTINUOUS_MODE(4),
|
||||
DEACTIVATE_CONTINUOUS_MODE(5);
|
||||
|
||||
private final int action;
|
||||
|
||||
OpenerAction(int action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public static @Nullable OpenerAction fromAction(int action) {
|
||||
for (OpenerAction value : values()) {
|
||||
if (value.action == action) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getAction() {
|
||||
return action;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.constants;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Enumeration of all lock actions Nuki Smart Lock accepts
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum SmartLockAction {
|
||||
UNLOCK(1),
|
||||
LOCK(2),
|
||||
UNLATCH(3),
|
||||
LOCK_N_GO(4),
|
||||
LOCK_N_GO_WITH_UNLATCH(5);
|
||||
|
||||
private final int action;
|
||||
|
||||
SmartLockAction(int action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public static @Nullable SmartLockAction fromAction(int action) {
|
||||
for (SmartLockAction value : values()) {
|
||||
if (value.action == action) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getAction() {
|
||||
return action;
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.converter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.openhab.binding.nuki.internal.NukiBindingConstants;
|
||||
|
||||
/**
|
||||
* The {@link LockActionConverter} is responsible for mapping Binding Lock States to Bridge HTTP-API Lock Actions.
|
||||
*
|
||||
* @author Markus Katter - Initial contribution
|
||||
*/
|
||||
public abstract class LockActionConverter {
|
||||
|
||||
private static Map<Integer, Integer> mapping;
|
||||
|
||||
private static void setupMapping() {
|
||||
mapping = new HashMap<>();
|
||||
mapping.put(NukiBindingConstants.LOCK_STATES_UNLOCKING, NukiBindingConstants.LOCK_ACTIONS_UNLOCK);
|
||||
mapping.put(NukiBindingConstants.LOCK_STATES_LOCKING, NukiBindingConstants.LOCK_ACTIONS_LOCK);
|
||||
mapping.put(NukiBindingConstants.LOCK_STATES_UNLATCHING, NukiBindingConstants.LOCK_ACTIONS_UNLATCH);
|
||||
mapping.put(NukiBindingConstants.LOCK_STATES_UNLOCKING_LOCKNGO,
|
||||
NukiBindingConstants.LOCK_ACTIONS_LOCKNGO_UNLOCK);
|
||||
mapping.put(NukiBindingConstants.LOCK_STATES_UNLATCHING_LOCKNGO,
|
||||
NukiBindingConstants.LOCK_ACTIONS_LOCKNGO_UNLATCH);
|
||||
}
|
||||
|
||||
public static int getLockActionFor(int lockState) {
|
||||
if (mapping == null) {
|
||||
setupMapping();
|
||||
}
|
||||
return mapping.get(lockState);
|
||||
}
|
||||
|
||||
public static int getLockStateFor(int lockAction) {
|
||||
if (mapping == null) {
|
||||
setupMapping();
|
||||
}
|
||||
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) {
|
||||
if (entry.getValue() == lockAction) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -12,8 +12,12 @@
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.dataexchange;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiCallbackListCallbackDto;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiCallbackListDto;
|
||||
|
||||
@ -22,15 +26,19 @@ import org.openhab.binding.nuki.internal.dto.BridgeApiCallbackListDto;
|
||||
*
|
||||
* @author Markus Katter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BridgeCallbackListResponse extends NukiBaseResponse {
|
||||
|
||||
private List<BridgeApiCallbackListCallbackDto> callbacks;
|
||||
private List<BridgeApiCallbackListCallbackDto> callbacks = Collections.emptyList();
|
||||
|
||||
public BridgeCallbackListResponse(int status, String message, BridgeApiCallbackListDto bridgeApiCallbackListDto) {
|
||||
public BridgeCallbackListResponse(int status, String message,
|
||||
@Nullable BridgeApiCallbackListDto bridgeApiCallbackListDto) {
|
||||
super(status, message);
|
||||
if (bridgeApiCallbackListDto != null) {
|
||||
this.setSuccess(true);
|
||||
this.callbacks = bridgeApiCallbackListDto.getCallbacks();
|
||||
if (bridgeApiCallbackListDto.getCallbacks() != null) {
|
||||
this.callbacks = bridgeApiCallbackListDto.getCallbacks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +50,11 @@ public class BridgeCallbackListResponse extends NukiBaseResponse {
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
public void setCallbacks(List<BridgeApiCallbackListCallbackDto> callbacks) {
|
||||
this.callbacks = callbacks;
|
||||
public void setCallbacks(@Nullable List<BridgeApiCallbackListCallbackDto> callbacks) {
|
||||
if (callbacks == null) {
|
||||
this.callbacks = new ArrayList<>();
|
||||
} else {
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.dataexchange;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiListDeviceDto;
|
||||
|
||||
/**
|
||||
* The {@link BridgeListResponse} class wraps {@link BridgeApiListDeviceDto} class.
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BridgeListResponse extends NukiBaseResponse {
|
||||
|
||||
private final List<BridgeApiListDeviceDto> devices;
|
||||
|
||||
public BridgeListResponse(int status, @Nullable String message, @Nullable List<BridgeApiListDeviceDto> devices) {
|
||||
super(status, message);
|
||||
setSuccess(devices != null);
|
||||
this.devices = devices == null ? Collections.emptyList() : Collections.unmodifiableList(devices);
|
||||
}
|
||||
|
||||
public BridgeListResponse(NukiBaseResponse nukiBaseResponse) {
|
||||
this(nukiBaseResponse.getStatus(), nukiBaseResponse.getMessage(), null);
|
||||
}
|
||||
|
||||
public List<BridgeApiListDeviceDto> getDevices() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
public @Nullable BridgeApiListDeviceDto getDevice(String nukiId) {
|
||||
for (BridgeApiListDeviceDto device : this.devices) {
|
||||
if (device.getNukiId().equals(nukiId)) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -22,55 +22,22 @@ import org.openhab.binding.nuki.internal.dto.BridgeApiLockStateDto;
|
||||
*/
|
||||
public class BridgeLockStateResponse extends NukiBaseResponse {
|
||||
|
||||
private int state;
|
||||
private String stateName;
|
||||
private boolean batteryCritical;
|
||||
private int doorsensorState;
|
||||
private final BridgeApiLockStateDto bridgeApiLockStateDto;
|
||||
|
||||
public BridgeLockStateResponse(int status, String message, BridgeApiLockStateDto bridgeApiLockStateDto) {
|
||||
super(status, message);
|
||||
this.bridgeApiLockStateDto = bridgeApiLockStateDto;
|
||||
if (bridgeApiLockStateDto != null) {
|
||||
this.setSuccess(bridgeApiLockStateDto.isSuccess());
|
||||
this.setState(bridgeApiLockStateDto.getState());
|
||||
this.setStateName(bridgeApiLockStateDto.getStateName());
|
||||
this.setDoorsensorState(bridgeApiLockStateDto.getDoorsensorState());
|
||||
this.setBatteryCritical(bridgeApiLockStateDto.isBatteryCritical());
|
||||
}
|
||||
}
|
||||
|
||||
public BridgeLockStateResponse(NukiBaseResponse nukiBaseResponse) {
|
||||
super(nukiBaseResponse.getStatus(), nukiBaseResponse.getMessage());
|
||||
this.bridgeApiLockStateDto = null;
|
||||
}
|
||||
|
||||
public int getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public String getStateName() {
|
||||
return stateName;
|
||||
}
|
||||
|
||||
public void setStateName(String stateName) {
|
||||
this.stateName = stateName;
|
||||
}
|
||||
|
||||
public boolean isBatteryCritical() {
|
||||
return batteryCritical;
|
||||
}
|
||||
|
||||
public void setBatteryCritical(boolean batteryCritical) {
|
||||
this.batteryCritical = batteryCritical;
|
||||
}
|
||||
|
||||
public int getDoorsensorState() {
|
||||
return doorsensorState;
|
||||
}
|
||||
|
||||
public void setDoorsensorState(int doorsensorState) {
|
||||
this.doorsensorState = doorsensorState;
|
||||
public BridgeApiLockStateDto getBridgeApiLockStateDto() {
|
||||
return bridgeApiLockStateDto;
|
||||
}
|
||||
}
|
||||
|
@ -12,30 +12,29 @@
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.dataexchange;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Dictionary;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.nuki.internal.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiLinkBuilder;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiLockStateRequestDto;
|
||||
import org.openhab.binding.nuki.internal.dto.NukiHttpServerStatusResponseDto;
|
||||
import org.openhab.binding.nuki.internal.handler.AbstractNukiDeviceHandler;
|
||||
import org.openhab.binding.nuki.internal.handler.NukiBridgeHandler;
|
||||
import org.openhab.binding.nuki.internal.handler.NukiSmartLockHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.types.State;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.osgi.service.http.NamespaceException;
|
||||
import org.slf4j.Logger;
|
||||
@ -48,34 +47,28 @@ import com.google.gson.Gson;
|
||||
*
|
||||
* @author Markus Katter - Initial contribution
|
||||
* @contributer Christian Hoefler - Door sensor integration
|
||||
* @contributer Jan Vybíral - Added Opener support, improved callback handling
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NukiApiServlet extends HttpServlet {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NukiApiServlet.class);
|
||||
private static final long serialVersionUID = -3601163473320027239L;
|
||||
private static final String CHARSET = "utf-8";
|
||||
private static final String APPLICATION_JSON = "application/json";
|
||||
|
||||
private HttpService httpService;
|
||||
private List<NukiBridgeHandler> nukiBridgeHandlers = new ArrayList<>();
|
||||
private String path;
|
||||
private Gson gson;
|
||||
private final HttpService httpService;
|
||||
private final List<NukiBridgeHandler> nukiBridgeHandlers = new ArrayList<>();
|
||||
private final Gson gson;
|
||||
|
||||
public NukiApiServlet(HttpService httpService) {
|
||||
logger.debug("Instantiating NukiApiServlet({})", httpService);
|
||||
this.httpService = httpService;
|
||||
gson = new Gson();
|
||||
}
|
||||
|
||||
public void add(@NonNull NukiBridgeHandler nukiBridgeHandler) {
|
||||
public void add(NukiBridgeHandler nukiBridgeHandler) {
|
||||
logger.trace("Adding NukiBridgeHandler[{}] for Bridge[{}] to NukiApiServlet.",
|
||||
nukiBridgeHandler.getThing().getUID(),
|
||||
nukiBridgeHandler.getThing().getConfiguration().get(NukiBindingConstants.CONFIG_IP));
|
||||
if (!nukiBridgeHandler.isInitializable()) {
|
||||
logger.debug("NukiBridgeHandler[{}] is not initializable, check required configuration!",
|
||||
nukiBridgeHandler.getThing().getUID());
|
||||
return;
|
||||
}
|
||||
if (nukiBridgeHandlers.isEmpty()) {
|
||||
this.activate();
|
||||
}
|
||||
@ -92,109 +85,83 @@ public class NukiApiServlet extends HttpServlet {
|
||||
}
|
||||
}
|
||||
|
||||
public int countNukiBridgeHandlers() {
|
||||
return nukiBridgeHandlers.size();
|
||||
}
|
||||
|
||||
private void activate() {
|
||||
logger.debug("Activating NukiApiServlet.");
|
||||
path = NukiBindingConstants.CALLBACK_ENDPOINT;
|
||||
Dictionary<String, String> servletParams = new Hashtable<>();
|
||||
try {
|
||||
httpService.registerServlet(path, this, servletParams, httpService.createDefaultHttpContext());
|
||||
logger.debug("Started NukiApiServlet at path[{}]", path);
|
||||
httpService.registerServlet(NukiLinkBuilder.CALLBACK_ENDPOINT, this, servletParams,
|
||||
httpService.createDefaultHttpContext());
|
||||
logger.debug("Started NukiApiServlet at path[{}]", NukiLinkBuilder.CALLBACK_ENDPOINT);
|
||||
} catch (ServletException | NamespaceException e) {
|
||||
logger.error("ERROR: {}", e.getMessage(), e);
|
||||
logger.error("Error activating NukiApiServlet: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void deactivate() {
|
||||
logger.trace("deactivate()");
|
||||
httpService.unregister(path);
|
||||
httpService.unregister(NukiLinkBuilder.CALLBACK_ENDPOINT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
logger.debug("Servlet Request at URI[{}] request[{}]", request.getRequestURI(), request);
|
||||
BridgeApiLockStateRequestDto bridgeApiLockStateRequestDto = getBridgeApiLockStateRequestDto(request);
|
||||
|
||||
ResponseEntity responseEntity;
|
||||
if (bridgeApiLockStateRequestDto == null) {
|
||||
logger.error("Could not handle Bridge CallBack Request - Discarding!");
|
||||
logger.error("Please report a bug, if this request was done by the Nuki Bridge!");
|
||||
setHeaders(response);
|
||||
response.setStatus(HttpStatus.BAD_REQUEST_400);
|
||||
response.getWriter().println(gson.toJson(new NukiHttpServerStatusResponseDto("Invalid BCB-Request!")));
|
||||
return;
|
||||
logger.warn(
|
||||
"Could not handle Bridge CallBack Request, Please report a bug, if this request was done by the Nuki Bridge! - {}",
|
||||
request);
|
||||
|
||||
responseEntity = new ResponseEntity(HttpStatus.BAD_REQUEST_400,
|
||||
new NukiHttpServerStatusResponseDto("Invalid BCB-Request!"));
|
||||
} else {
|
||||
responseEntity = doHandle(bridgeApiLockStateRequestDto, request.getParameter("bridgeId"));
|
||||
}
|
||||
String nukiId = String.format("%08X", (bridgeApiLockStateRequestDto.getNukiId()));
|
||||
String nukiIdThing;
|
||||
|
||||
setHeaders(response);
|
||||
response.setStatus(responseEntity.getStatus());
|
||||
response.getWriter().write(gson.toJson(responseEntity.getData()));
|
||||
}
|
||||
|
||||
private ResponseEntity doHandle(BridgeApiLockStateRequestDto request, @Nullable String bridgeId) {
|
||||
String nukiId = request.getNukiId().toString();
|
||||
for (NukiBridgeHandler nukiBridgeHandler : nukiBridgeHandlers) {
|
||||
logger.trace("Searching Bridge[{}] with NukiBridgeHandler[{}] for nukiId[{}].",
|
||||
nukiBridgeHandler.getThing().getConfiguration().get(NukiBindingConstants.CONFIG_IP),
|
||||
nukiBridgeHandler.getThing().getUID(), nukiId);
|
||||
List<@NonNull Thing> allSmartLocks = nukiBridgeHandler.getThing().getThings();
|
||||
List<Thing> allSmartLocks = nukiBridgeHandler.getThing().getThings();
|
||||
for (Thing thing : allSmartLocks) {
|
||||
nukiIdThing = thing.getConfiguration().containsKey(NukiBindingConstants.CONFIG_NUKI_ID)
|
||||
? (String) thing.getConfiguration().get(NukiBindingConstants.CONFIG_NUKI_ID)
|
||||
: null;
|
||||
if (nukiIdThing != null && nukiIdThing.equals(nukiId)) {
|
||||
String nukiIdThing = String
|
||||
.valueOf(thing.getConfiguration().get(NukiBindingConstants.PROPERTY_NUKI_ID));
|
||||
if (nukiIdThing.equals(nukiId)) {
|
||||
logger.debug("Processing ThingUID[{}] - nukiId[{}]", thing.getUID(), nukiId);
|
||||
NukiSmartLockHandler nsh = getSmartLockHandler(thing);
|
||||
if (nsh == null) {
|
||||
logger.debug("Could not update channels for ThingUID[{}] because Handler is null!",
|
||||
thing.getUID());
|
||||
break;
|
||||
AbstractNukiDeviceHandler<?> nsh = getDeviceHandler(thing);
|
||||
if (nsh != null) {
|
||||
nsh.refreshState(request);
|
||||
return new ResponseEntity(HttpStatus.OK_200, new NukiHttpServerStatusResponseDto("OK"));
|
||||
}
|
||||
Channel channel = thing.getChannel(NukiBindingConstants.CHANNEL_SMARTLOCK_LOCK);
|
||||
if (channel != null) {
|
||||
State state = bridgeApiLockStateRequestDto.getState() == NukiBindingConstants.LOCK_STATES_LOCKED
|
||||
? OnOffType.ON
|
||||
: OnOffType.OFF;
|
||||
nsh.handleApiServletUpdate(channel.getUID(), state);
|
||||
}
|
||||
channel = thing.getChannel(NukiBindingConstants.CHANNEL_SMARTLOCK_STATE);
|
||||
if (channel != null) {
|
||||
State state = new DecimalType(bridgeApiLockStateRequestDto.getState());
|
||||
nsh.handleApiServletUpdate(channel.getUID(), state);
|
||||
}
|
||||
channel = thing.getChannel(NukiBindingConstants.CHANNEL_SMARTLOCK_LOW_BATTERY);
|
||||
if (channel != null) {
|
||||
State state = bridgeApiLockStateRequestDto.isBatteryCritical() ? OnOffType.ON : OnOffType.OFF;
|
||||
nsh.handleApiServletUpdate(channel.getUID(), state);
|
||||
}
|
||||
channel = thing.getChannel(NukiBindingConstants.CHANNEL_SMARTLOCK_DOOR_STATE);
|
||||
if (channel != null) {
|
||||
State state = new DecimalType(bridgeApiLockStateRequestDto.getDoorsensorState());
|
||||
nsh.handleApiServletUpdate(channel.getUID(), state);
|
||||
}
|
||||
setHeaders(response);
|
||||
response.getWriter().println(gson.toJson(new NukiHttpServerStatusResponseDto("OK")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Smart Lock with nukiId[{}] not found!", nukiId);
|
||||
setHeaders(response);
|
||||
response.setStatus(HttpStatus.NOT_FOUND_404);
|
||||
response.getWriter().println(gson.toJson(new NukiHttpServerStatusResponseDto("Smart Lock not found!")));
|
||||
return new ResponseEntity(HttpStatus.NOT_FOUND_404,
|
||||
new NukiHttpServerStatusResponseDto("Smart Lock not found!"));
|
||||
}
|
||||
|
||||
private BridgeApiLockStateRequestDto getBridgeApiLockStateRequestDto(HttpServletRequest request) {
|
||||
logger.trace("getBridgeApiLockStateRequestDto(...)");
|
||||
private @Nullable BridgeApiLockStateRequestDto getBridgeApiLockStateRequestDto(HttpServletRequest request) {
|
||||
String requestContent = null;
|
||||
try {
|
||||
requestContent = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
|
||||
try (BufferedReader reader = request.getReader()) {
|
||||
requestContent = readAll(reader);
|
||||
BridgeApiLockStateRequestDto bridgeApiLockStateRequestDto = gson.fromJson(requestContent,
|
||||
BridgeApiLockStateRequestDto.class);
|
||||
if (bridgeApiLockStateRequestDto.getNukiId() != 0) {
|
||||
if (bridgeApiLockStateRequestDto != null && bridgeApiLockStateRequestDto.getNukiId() != null) {
|
||||
logger.trace("requestContent[{}]", requestContent);
|
||||
return bridgeApiLockStateRequestDto;
|
||||
} else {
|
||||
logger.error("Invalid BCB-Request payload data!");
|
||||
logger.error("requestContent[{}]", requestContent);
|
||||
logger.debug("Invalid BCB-Request payload data! {}", requestContent);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not read payload from BCB-Request! Message[{}]", e.getMessage());
|
||||
logger.debug("Could not read payload from BCB-Request! Message[{}]", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("Could not create BridgeApiLockStateRequestDto from BCB-Request! Message[{}]", e.getMessage());
|
||||
logger.error("requestContent[{}]", requestContent);
|
||||
@ -202,18 +169,45 @@ public class NukiApiServlet extends HttpServlet {
|
||||
return null;
|
||||
}
|
||||
|
||||
private NukiSmartLockHandler getSmartLockHandler(Thing thing) {
|
||||
logger.trace("getSmartLockHandler(...) from thing[{}]", thing.getUID());
|
||||
NukiSmartLockHandler nsh = (NukiSmartLockHandler) thing.getHandler();
|
||||
private String readAll(BufferedReader reader) throws IOException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char[] buffer = new char[1024];
|
||||
int read = 0;
|
||||
while ((read = reader.read(buffer)) != -1) {
|
||||
sb.append(buffer, 0, read);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private @Nullable AbstractNukiDeviceHandler<?> getDeviceHandler(Thing thing) {
|
||||
AbstractNukiDeviceHandler<?> nsh = (AbstractNukiDeviceHandler<?>) thing.getHandler();
|
||||
if (nsh == null) {
|
||||
logger.debug("Could not get NukiSmartLockHandler for ThingUID[{}]!", thing.getUID());
|
||||
logger.debug("Could not get AbstractNukiDeviceHandler for ThingUID[{}]!", thing.getUID());
|
||||
return null;
|
||||
}
|
||||
return nsh;
|
||||
}
|
||||
|
||||
private void setHeaders(HttpServletResponse response) {
|
||||
response.setCharacterEncoding(CHARSET);
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.setContentType(APPLICATION_JSON);
|
||||
}
|
||||
|
||||
private static class ResponseEntity {
|
||||
private final int status;
|
||||
private final Object data;
|
||||
|
||||
private ResponseEntity(int status, Object data) {
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public Object getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,19 +14,24 @@ package org.openhab.binding.nuki.internal.dataexchange;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link NukiBaseResponse} class is the base class for API Responses.
|
||||
*
|
||||
* @author Markus Katter - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NukiBaseResponse {
|
||||
|
||||
private int status;
|
||||
@Nullable
|
||||
private String message;
|
||||
private boolean success;
|
||||
private Instant created;
|
||||
|
||||
public NukiBaseResponse(int status, String message) {
|
||||
public NukiBaseResponse(int status, @Nullable String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
this.created = Instant.now();
|
||||
@ -40,7 +45,7 @@ public class NukiBaseResponse {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
public @Nullable String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
|
@ -13,25 +13,28 @@
|
||||
package org.openhab.binding.nuki.internal.dataexchange;
|
||||
|
||||
import java.io.InterruptedIOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.SocketException;
|
||||
import java.net.URLEncoder;
|
||||
import java.time.Instant;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpResponseException;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.nuki.internal.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiLinkBuilder;
|
||||
import org.openhab.binding.nuki.internal.constants.OpenerAction;
|
||||
import org.openhab.binding.nuki.internal.constants.SmartLockAction;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiCallbackAddDto;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiCallbackListDto;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiCallbackRemoveDto;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiInfoDto;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiListDeviceDto;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiLockActionDto;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiLockStateDto;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -41,45 +44,27 @@ import com.google.gson.Gson;
|
||||
* The {@link NukiHttpClient} class is responsible for getting data from the Nuki Bridge.
|
||||
*
|
||||
* @author Markus Katter - Initial contribution
|
||||
* @contributer Jan Vybíral - Hashed token authentication
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NukiHttpClient {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NukiHttpClient.class);
|
||||
private static final long CACHE_PERIOD = 5;
|
||||
|
||||
private HttpClient httpClient;
|
||||
private Configuration configuration;
|
||||
private Gson gson;
|
||||
private BridgeLockStateResponse bridgeLockStateResponseCache;
|
||||
private final HttpClient httpClient;
|
||||
private final Gson gson;
|
||||
private final NukiLinkBuilder linkBuilder;
|
||||
|
||||
public NukiHttpClient(HttpClient httpClient, Configuration configuration) {
|
||||
logger.debug("Instantiating NukiHttpClient({})", configuration);
|
||||
this.configuration = configuration;
|
||||
public NukiHttpClient(HttpClient httpClient, NukiLinkBuilder linkBuilder) {
|
||||
logger.debug("Instantiating NukiHttpClient");
|
||||
this.httpClient = httpClient;
|
||||
this.linkBuilder = linkBuilder;
|
||||
gson = new Gson();
|
||||
}
|
||||
|
||||
private String prepareUri(String uriTemplate, String... additionalArguments) {
|
||||
String configIp = (String) configuration.get(NukiBindingConstants.CONFIG_IP);
|
||||
BigDecimal configPort = (BigDecimal) configuration.get(NukiBindingConstants.CONFIG_PORT);
|
||||
String configApiToken = (String) configuration.get(NukiBindingConstants.CONFIG_API_TOKEN);
|
||||
String[] parameters = new String[additionalArguments.length + 3];
|
||||
parameters[0] = configIp;
|
||||
parameters[1] = configPort.toString();
|
||||
parameters[2] = configApiToken;
|
||||
System.arraycopy(additionalArguments, 0, parameters, 3, additionalArguments.length);
|
||||
String uri = String.format(uriTemplate, parameters);
|
||||
logger.trace("prepareUri(...):URI[{}]", uri);
|
||||
return uri;
|
||||
}
|
||||
|
||||
private synchronized ContentResponse executeRequest(String uri)
|
||||
private synchronized ContentResponse executeRequest(URI uri)
|
||||
throws InterruptedException, ExecutionException, TimeoutException {
|
||||
logger.debug("executeRequest({})", uri);
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
ContentResponse contentResponse = httpClient.GET(uri);
|
||||
logger.debug("contentResponseAsString[{}]", contentResponse.getContentAsString());
|
||||
return contentResponse;
|
||||
@ -87,19 +72,20 @@ public class NukiHttpClient {
|
||||
|
||||
private NukiBaseResponse handleException(Exception e) {
|
||||
if (e instanceof ExecutionException) {
|
||||
if (e.getCause() instanceof HttpResponseException) {
|
||||
HttpResponseException cause = (HttpResponseException) e.getCause();
|
||||
int status = cause.getResponse().getStatus();
|
||||
String reason = cause.getResponse().getReason();
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof HttpResponseException) {
|
||||
HttpResponseException causeException = (HttpResponseException) cause;
|
||||
int status = causeException.getResponse().getStatus();
|
||||
String reason = causeException.getResponse().getReason();
|
||||
logger.debug("HTTP Response Exception! Status[{}] - Reason[{}]! Check your API Token!", status, reason);
|
||||
return new NukiBaseResponse(status, reason);
|
||||
} else if (e.getCause() instanceof InterruptedIOException) {
|
||||
} else if (cause instanceof InterruptedIOException) {
|
||||
logger.debug(
|
||||
"InterruptedIOException! Exception[{}]! Check IP/Port configuration and if Nuki Bridge is powered on!",
|
||||
e.getMessage());
|
||||
return new NukiBaseResponse(HttpStatus.REQUEST_TIMEOUT_408,
|
||||
"InterruptedIOException! Check IP/Port configuration and if Nuki Bridge is powered on!");
|
||||
} else if (e.getCause() instanceof SocketException) {
|
||||
} else if (cause instanceof SocketException) {
|
||||
logger.debug(
|
||||
"SocketException! Exception[{}]! Check IP/Port configuration and if Nuki Bridge is powered on!",
|
||||
e.getMessage());
|
||||
@ -113,9 +99,8 @@ public class NukiHttpClient {
|
||||
|
||||
public BridgeInfoResponse getBridgeInfo() {
|
||||
logger.debug("getBridgeInfo() in thread {}", Thread.currentThread().getId());
|
||||
String uri = prepareUri(NukiBindingConstants.URI_INFO);
|
||||
try {
|
||||
ContentResponse contentResponse = executeRequest(uri);
|
||||
ContentResponse contentResponse = executeRequest(linkBuilder.info());
|
||||
int status = contentResponse.getStatus();
|
||||
String response = contentResponse.getContentAsString();
|
||||
logger.debug("getBridgeInfo status[{}] response[{}]", status, response);
|
||||
@ -133,43 +118,63 @@ public class NukiHttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
public BridgeLockStateResponse getBridgeLockState(String nukiId) {
|
||||
logger.debug("getBridgeLockState({}) in thread {}", nukiId, Thread.currentThread().getId());
|
||||
long timestampSecs = Instant.now().getEpochSecond();
|
||||
if (this.bridgeLockStateResponseCache != null
|
||||
&& timestampSecs < this.bridgeLockStateResponseCache.getCreated().getEpochSecond() + CACHE_PERIOD) {
|
||||
logger.debug("Returning LockState from cache - now[{}]<created[{}]+cachePeriod[{}]", timestampSecs,
|
||||
this.bridgeLockStateResponseCache.getCreated().getEpochSecond(), CACHE_PERIOD);
|
||||
return bridgeLockStateResponseCache;
|
||||
} else {
|
||||
logger.debug("Requesting LockState from Bridge.");
|
||||
}
|
||||
String uri = prepareUri(NukiBindingConstants.URI_LOCKSTATE, nukiId);
|
||||
public BridgeListResponse getList() {
|
||||
logger.debug("getList() in thread {}", Thread.currentThread().getId());
|
||||
try {
|
||||
ContentResponse contentResponse = executeRequest(uri);
|
||||
ContentResponse contentResponse = executeRequest(linkBuilder.list());
|
||||
int status = contentResponse.getStatus();
|
||||
String response = contentResponse.getContentAsString();
|
||||
logger.debug("getList status[{}] response[{}]", status, response);
|
||||
if (status == HttpStatus.OK_200) {
|
||||
BridgeApiListDeviceDto[] bridgeApiInfoDtoArray = gson.fromJson(response,
|
||||
BridgeApiListDeviceDto[].class);
|
||||
logger.debug("getList OK");
|
||||
return new BridgeListResponse(status, contentResponse.getReason(),
|
||||
Arrays.asList(bridgeApiInfoDtoArray));
|
||||
} else {
|
||||
logger.debug("Could not get Bridge Info! Status[{}] - Response[{}]", status, response);
|
||||
return new BridgeListResponse(status, contentResponse.getReason(), null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("Could not get List! Exception[{}]", e.getMessage());
|
||||
return new BridgeListResponse(handleException(e));
|
||||
}
|
||||
}
|
||||
|
||||
public BridgeLockStateResponse getBridgeLockState(String nukiId, int deviceType) {
|
||||
logger.debug("getBridgeLockState({}) in thread {}", nukiId, Thread.currentThread().getId());
|
||||
|
||||
try {
|
||||
ContentResponse contentResponse = executeRequest(linkBuilder.lockState(nukiId, deviceType));
|
||||
int status = contentResponse.getStatus();
|
||||
String response = contentResponse.getContentAsString();
|
||||
logger.debug("getBridgeLockState status[{}] response[{}]", status, response);
|
||||
if (status == HttpStatus.OK_200) {
|
||||
BridgeApiLockStateDto bridgeApiLockStateDto = gson.fromJson(response, BridgeApiLockStateDto.class);
|
||||
logger.debug("getBridgeLockState OK");
|
||||
return bridgeLockStateResponseCache = new BridgeLockStateResponse(status, contentResponse.getReason(),
|
||||
bridgeApiLockStateDto);
|
||||
return new BridgeLockStateResponse(status, contentResponse.getReason(), bridgeApiLockStateDto);
|
||||
} else {
|
||||
logger.debug("Could not get Lock State! Status[{}] - Response[{}]", status, response);
|
||||
return new BridgeLockStateResponse(status, contentResponse.getReason(), null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("Could not get Bridge Lock State! Exception[{}]", e.getMessage());
|
||||
logger.debug("Could not get Bridge Lock State!", e);
|
||||
return new BridgeLockStateResponse(handleException(e));
|
||||
}
|
||||
}
|
||||
|
||||
public BridgeLockActionResponse getBridgeLockAction(String nukiId, int lockAction) {
|
||||
public BridgeLockActionResponse getSmartLockAction(String nukiId, SmartLockAction action) {
|
||||
return getBridgeLockAction(nukiId, action.getAction(), NukiBindingConstants.DEVICE_SMART_LOCK);
|
||||
}
|
||||
|
||||
public BridgeLockActionResponse getOpenerAction(String nukiId, OpenerAction action) {
|
||||
return getBridgeLockAction(nukiId, action.getAction(), NukiBindingConstants.DEVICE_OPENER);
|
||||
}
|
||||
|
||||
private BridgeLockActionResponse getBridgeLockAction(String nukiId, int lockAction, int deviceType) {
|
||||
logger.debug("getBridgeLockAction({}, {}) in thread {}", nukiId, lockAction, Thread.currentThread().getId());
|
||||
String uri = prepareUri(NukiBindingConstants.URI_LOCKACTION, nukiId, Integer.toString(lockAction));
|
||||
try {
|
||||
ContentResponse contentResponse = executeRequest(uri);
|
||||
ContentResponse contentResponse = executeRequest(linkBuilder.lockAction(nukiId, deviceType, lockAction));
|
||||
int status = contentResponse.getStatus();
|
||||
String response = contentResponse.getContentAsString();
|
||||
logger.debug("getBridgeLockAction status[{}] response[{}]", status, response);
|
||||
@ -190,8 +195,7 @@ public class NukiHttpClient {
|
||||
public BridgeCallbackAddResponse getBridgeCallbackAdd(String callbackUrl) {
|
||||
logger.debug("getBridgeCallbackAdd({}) in thread {}", callbackUrl, Thread.currentThread().getId());
|
||||
try {
|
||||
String uri = prepareUri(NukiBindingConstants.URI_CBADD, URLEncoder.encode(callbackUrl, "UTF-8"));
|
||||
ContentResponse contentResponse = executeRequest(uri);
|
||||
ContentResponse contentResponse = executeRequest(linkBuilder.callbackAdd(callbackUrl));
|
||||
int status = contentResponse.getStatus();
|
||||
String response = contentResponse.getContentAsString();
|
||||
logger.debug("getBridgeCallbackAdd status[{}] response[{}]", status, response);
|
||||
@ -212,9 +216,8 @@ public class NukiHttpClient {
|
||||
|
||||
public BridgeCallbackListResponse getBridgeCallbackList() {
|
||||
logger.debug("getBridgeCallbackList() in thread {}", Thread.currentThread().getId());
|
||||
String uri = prepareUri(NukiBindingConstants.URI_CBLIST);
|
||||
try {
|
||||
ContentResponse contentResponse = executeRequest(uri);
|
||||
ContentResponse contentResponse = executeRequest(linkBuilder.callbackList());
|
||||
int status = contentResponse.getStatus();
|
||||
String response = contentResponse.getContentAsString();
|
||||
logger.debug("getBridgeCallbackList status[{}] response[{}]", status, response);
|
||||
@ -236,8 +239,7 @@ public class NukiHttpClient {
|
||||
public BridgeCallbackRemoveResponse getBridgeCallbackRemove(int id) {
|
||||
logger.debug("getBridgeCallbackRemove({}) in thread {}", id, Thread.currentThread().getId());
|
||||
try {
|
||||
String uri = prepareUri(NukiBindingConstants.URI_CBREMOVE, Integer.toString(id));
|
||||
ContentResponse contentResponse = executeRequest(uri);
|
||||
ContentResponse contentResponse = executeRequest(linkBuilder.callbackRemove(id));
|
||||
int status = contentResponse.getStatus();
|
||||
String response = contentResponse.getContentAsString();
|
||||
logger.debug("getBridgeCallbackRemove status[{}] response[{}]", status, response);
|
||||
|
@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.discovery;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiLinkBuilder;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiAuthDto;
|
||||
import org.openhab.binding.nuki.internal.dto.WebApiBridgeDiscoveryDto;
|
||||
import org.openhab.binding.nuki.internal.dto.WebApiBridgeDto;
|
||||
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.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.ThingRegistry;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* Discovery service which uses Nuki Web API to discover all bridges on same network
|
||||
* and uses authentication API to obtain access token.
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, configurationPid = "discovery." + NukiBindingConstants.BINDING_ID)
|
||||
public class NukiBridgeDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NukiBridgeDiscoveryService.class);
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final ThingRegistry thingRegistry;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
@Activate
|
||||
public NukiBridgeDiscoveryService(@Reference final HttpClientFactory httpClientFactory,
|
||||
@Reference final ThingRegistry thingRegistry) {
|
||||
super(Collections.singleton(NukiBindingConstants.THING_TYPE_BRIDGE), 30, false);
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
this.thingRegistry = thingRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
try {
|
||||
ContentResponse response = this.httpClient.GET(NukiLinkBuilder.URI_BRIDGE_DISCOVERY);
|
||||
if (response.getStatus() == HttpStatus.OK_200) {
|
||||
String responseString = response.getContentAsString();
|
||||
WebApiBridgeDiscoveryDto discoveryResult = gson.fromJson(responseString,
|
||||
WebApiBridgeDiscoveryDto.class);
|
||||
if (discoveryResult == null) {
|
||||
logger.debug("Bridge discovery failed - API returned invalid body {}", responseString);
|
||||
} else if (discoveryResult.getErrorCode() == 0) {
|
||||
discoverBridges(discoveryResult);
|
||||
} else {
|
||||
logger.debug("Bridge discovery failed - API returned error code '{}': {}",
|
||||
discoveryResult.getErrorCode(), responseString);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Bridge discovery failed - invalid status {}: '{}'", response.getStatus(),
|
||||
response.getContentAsString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("Bridge discovery failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void discoverBridges(WebApiBridgeDiscoveryDto discoveryResult) {
|
||||
logger.debug("Discovery finished, found {} bridges", discoveryResult);
|
||||
|
||||
discoveryResult.getBridges().forEach(bridge -> {
|
||||
if (thingRegistry.get(bridge.getThingUid()) != null) {
|
||||
logger.debug("Bridge {} already exists, skipping discovery", bridge.getThingUid());
|
||||
} else {
|
||||
scheduler.execute(new BridgeInitializer(bridge));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void discoverBridge(WebApiBridgeDto bridgeData, String token) {
|
||||
String name;
|
||||
if (token.isBlank()) {
|
||||
logger.debug("Nuki bridge {}({}) discovered without api token", bridgeData.getIp(),
|
||||
bridgeData.getBridgeId());
|
||||
name = "Nuki Bridge (no API token)";
|
||||
} else {
|
||||
logger.info("Nuki bridge {}({}) discovered and initialized", bridgeData.getIp(), bridgeData.getBridgeId());
|
||||
name = "Nuki Bridge";
|
||||
}
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(bridgeData.getThingUid()).withLabel(name)
|
||||
.withProperty(NukiBindingConstants.PROPERTY_BRIDGE_ID, bridgeData.getBridgeId())
|
||||
.withProperty(NukiBindingConstants.CONFIG_IP, bridgeData.getIp())
|
||||
.withProperty(NukiBindingConstants.CONFIG_PORT, bridgeData.getPort())
|
||||
.withProperty(NukiBindingConstants.CONFIG_API_TOKEN, token)
|
||||
.withRepresentationProperty(NukiBindingConstants.PROPERTY_BRIDGE_ID).build();
|
||||
thingDiscovered(result);
|
||||
}
|
||||
|
||||
private class BridgeInitializer implements Runnable {
|
||||
private final WebApiBridgeDto bridge;
|
||||
|
||||
private BridgeInitializer(WebApiBridgeDto bridge) {
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
logger.info("Discovered Nuki bridge {}({}) - obtaining API token, press button on bridge to complete",
|
||||
bridge.getIp(), bridge.getBridgeId());
|
||||
try {
|
||||
ContentResponse response = httpClient.GET(NukiLinkBuilder.getAuthUri(bridge.getIp(), bridge.getPort()));
|
||||
String responseData = response.getContentAsString();
|
||||
if (response.getStatus() == HttpStatus.OK_200) {
|
||||
BridgeApiAuthDto authResult = gson.fromJson(responseData, BridgeApiAuthDto.class);
|
||||
if (authResult != null && authResult.isSuccess()) {
|
||||
discoverBridge(bridge, authResult.getToken());
|
||||
} else {
|
||||
logger.warn(
|
||||
"Failed to get API token for bridge {}({}) - bridge did not return success response, make sure button on bridge was pressed during discovery",
|
||||
bridge.getIp(), bridge.getBridgeId());
|
||||
discoverBridge(bridge, "");
|
||||
}
|
||||
} else if (response.getStatus() == HttpStatus.FORBIDDEN_403) {
|
||||
logger.warn(
|
||||
"Failed to get API token for bridge {}({}) - bridge authentication is disabled, check settings",
|
||||
bridge.getIp(), bridge.getBridgeId());
|
||||
discoverBridge(bridge, "");
|
||||
} else {
|
||||
logger.warn("Failed to get API token for bridge {}({}) - invalid status {}: {}", bridge.getIp(),
|
||||
bridge.getBridgeId(), response.getStatus(), responseData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to get API token for bridge {}({})", bridge.getIp(), bridge.getBridgeId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.discovery;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.BridgeListResponse;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiListDeviceDto;
|
||||
import org.openhab.binding.nuki.internal.handler.NukiBridgeHandler;
|
||||
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.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Discovery service which uses Brige API to find all devices connected to bridges.
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NukiDeviceDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NukiDeviceDiscoveryService.class);
|
||||
@Nullable
|
||||
private NukiBridgeHandler bridge;
|
||||
|
||||
public NukiDeviceDiscoveryService() {
|
||||
super(Set.of(NukiBindingConstants.THING_TYPE_SMARTLOCK), 5, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
NukiBridgeHandler bridgeHandler = bridge;
|
||||
if (bridgeHandler == null) {
|
||||
logger.warn("Cannot start Nuki discovery - no bridge available");
|
||||
return;
|
||||
}
|
||||
|
||||
scheduler.execute(() -> {
|
||||
bridgeHandler.withHttpClient(client -> {
|
||||
BridgeListResponse list = client.getList();
|
||||
list.getDevices().stream()
|
||||
.filter(device -> NukiBindingConstants.SUPPORTED_DEVICES.contains(device.getDeviceType()))
|
||||
.map(device -> createDiscoveryResult(device, bridgeHandler)).forEach(this::thingDiscovered);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private DiscoveryResult createDiscoveryResult(BridgeApiListDeviceDto device, NukiBridgeHandler bridgeHandler) {
|
||||
return DiscoveryResultBuilder.create(getUid(device.getNukiId(), device.getDeviceType(), bridgeHandler))
|
||||
.withBridge(bridgeHandler.getThing().getUID()).withLabel(device.getName())
|
||||
.withRepresentationProperty(NukiBindingConstants.PROPERTY_NUKI_ID)
|
||||
.withProperty(NukiBindingConstants.PROPERTY_NAME, device.getName())
|
||||
.withProperty(NukiBindingConstants.PROPERTY_NUKI_ID, device.getNukiId())
|
||||
.withProperty(NukiBindingConstants.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion()).build();
|
||||
}
|
||||
|
||||
private ThingUID getUid(String nukiId, int deviceType, NukiBridgeHandler bridgeHandler) {
|
||||
if (deviceType == NukiBindingConstants.DEVICE_OPENER) {
|
||||
return new ThingUID(NukiBindingConstants.THING_TYPE_OPENER, bridgeHandler.getThing().getUID(), nukiId);
|
||||
} else {
|
||||
return new ThingUID(NukiBindingConstants.THING_TYPE_SMARTLOCK, bridgeHandler.getThing().getUID(), nukiId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void stopScan() {
|
||||
super.stopScan();
|
||||
removeOlderResults(getTimestampOfLastScan());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
if (handler instanceof NukiBridgeHandler) {
|
||||
bridge = (NukiBridgeHandler) handler;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return bridge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.dto;
|
||||
|
||||
/**
|
||||
* The {@link BridgeApiAuthDto} class defines the Data Transfer Object (POJO)
|
||||
* for the Nuki Bridge API /auth endpoint.
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
public class BridgeApiAuthDto {
|
||||
private String token;
|
||||
private boolean success;
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public void setSuccess(boolean success) {
|
||||
this.success = success;
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.dto;
|
||||
|
||||
/**
|
||||
* Base class for responses with Nuki device state
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
public class BridgeApiDeviceStateDto {
|
||||
private int mode;
|
||||
private int state;
|
||||
private String stateName;
|
||||
private boolean batteryCritical;
|
||||
private Boolean batteryCharging;
|
||||
private Integer batteryChargeState;
|
||||
private Boolean keypadBatteryCritical;
|
||||
private Integer doorsensorState;
|
||||
private String doorsensorStateName;
|
||||
private String ringactionTimestamp;
|
||||
private Boolean ringactionState;
|
||||
|
||||
public int getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(int mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public int getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public String getStateName() {
|
||||
return stateName;
|
||||
}
|
||||
|
||||
public void setStateName(String stateName) {
|
||||
this.stateName = stateName;
|
||||
}
|
||||
|
||||
public boolean isBatteryCritical() {
|
||||
return batteryCritical;
|
||||
}
|
||||
|
||||
public void setBatteryCritical(boolean batteryCritical) {
|
||||
this.batteryCritical = batteryCritical;
|
||||
}
|
||||
|
||||
public Boolean getBatteryCharging() {
|
||||
return batteryCharging;
|
||||
}
|
||||
|
||||
public void setBatteryCharging(Boolean batteryCharging) {
|
||||
this.batteryCharging = batteryCharging;
|
||||
}
|
||||
|
||||
public Integer getBatteryChargeState() {
|
||||
return batteryChargeState;
|
||||
}
|
||||
|
||||
public void setBatteryChargeState(Integer batteryChargeState) {
|
||||
this.batteryChargeState = batteryChargeState;
|
||||
}
|
||||
|
||||
public Boolean getKeypadBatteryCritical() {
|
||||
return keypadBatteryCritical;
|
||||
}
|
||||
|
||||
public void setKeypadBatteryCritical(Boolean keypadBatteryCritical) {
|
||||
this.keypadBatteryCritical = keypadBatteryCritical;
|
||||
}
|
||||
|
||||
public Integer getDoorsensorState() {
|
||||
return doorsensorState;
|
||||
}
|
||||
|
||||
public void setDoorsensorState(Integer doorsensorState) {
|
||||
this.doorsensorState = doorsensorState;
|
||||
}
|
||||
|
||||
public String getRingactionTimestamp() {
|
||||
return ringactionTimestamp;
|
||||
}
|
||||
|
||||
public void setRingactionTimestamp(String ringactionTimestamp) {
|
||||
this.ringactionTimestamp = ringactionTimestamp;
|
||||
}
|
||||
|
||||
public Boolean getRingactionState() {
|
||||
return ringactionState;
|
||||
}
|
||||
|
||||
public void setRingactionState(Boolean ringactionState) {
|
||||
this.ringactionState = ringactionState;
|
||||
}
|
||||
|
||||
public String getDoorsensorStateName() {
|
||||
return doorsensorStateName;
|
||||
}
|
||||
|
||||
public void setDoorsensorStateName(String doorsensorStateName) {
|
||||
this.doorsensorStateName = doorsensorStateName;
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.dto;
|
||||
|
||||
/**
|
||||
* The {@link BridgeApiListDeviceDto} class defines the Data Transfer Object (POJO) for the Nuki Bridge API /list
|
||||
* endpoint.
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
public class BridgeApiListDeviceDto {
|
||||
|
||||
private String nukiId;
|
||||
private String firmwareVersion;
|
||||
private int deviceType;
|
||||
private String name;
|
||||
private BridgeApiListDeviceLastKnownState lastKnownState;
|
||||
|
||||
public String getNukiId() {
|
||||
return nukiId;
|
||||
}
|
||||
|
||||
public void setNukiId(String nukiId) {
|
||||
this.nukiId = nukiId;
|
||||
}
|
||||
|
||||
public int getDeviceType() {
|
||||
return deviceType;
|
||||
}
|
||||
|
||||
public void setDeviceType(int deviceType) {
|
||||
this.deviceType = deviceType;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public BridgeApiListDeviceLastKnownState getLastKnownState() {
|
||||
return lastKnownState;
|
||||
}
|
||||
|
||||
public void setLastKnownState(BridgeApiListDeviceLastKnownState lastKnownState) {
|
||||
this.lastKnownState = lastKnownState;
|
||||
}
|
||||
|
||||
public String getFirmwareVersion() {
|
||||
return firmwareVersion;
|
||||
}
|
||||
|
||||
public void setFirmwareVersion(String firmwareVersion) {
|
||||
this.firmwareVersion = firmwareVersion;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.dto;
|
||||
|
||||
/**
|
||||
* The {@link BridgeApiListDeviceLastKnownState} class defines the Data Transfer Object (POJO) for the Nuki Bridge API
|
||||
* lastKnownState of {@link BridgeApiListDeviceDto}.
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
public class BridgeApiListDeviceLastKnownState extends BridgeApiLockStateDto {
|
||||
|
||||
private String timestamp;
|
||||
|
||||
public String getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(String timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
@ -19,46 +19,9 @@ package org.openhab.binding.nuki.internal.dto;
|
||||
* @author Markus Katter - Initial contribution
|
||||
* @contributer Christian Hoefler - Door sensor integration
|
||||
*/
|
||||
public class BridgeApiLockStateDto {
|
||||
|
||||
private int state;
|
||||
private String stateName;
|
||||
private boolean batteryCritical;
|
||||
private int doorsensorState;
|
||||
public class BridgeApiLockStateDto extends BridgeApiDeviceStateDto {
|
||||
private boolean success;
|
||||
|
||||
public int getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public String getStateName() {
|
||||
return stateName;
|
||||
}
|
||||
|
||||
public void setStateName(String stateName) {
|
||||
this.stateName = stateName;
|
||||
}
|
||||
|
||||
public boolean isBatteryCritical() {
|
||||
return batteryCritical;
|
||||
}
|
||||
|
||||
public void setBatteryCritical(boolean batteryCritical) {
|
||||
this.batteryCritical = batteryCritical;
|
||||
}
|
||||
|
||||
public int getDoorsensorState() {
|
||||
return doorsensorState;
|
||||
}
|
||||
|
||||
public void setDoorsensorState(int doorsensorState) {
|
||||
this.doorsensorState = doorsensorState;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
@ -19,51 +19,15 @@ package org.openhab.binding.nuki.internal.dto;
|
||||
* @author Markus Katter - Initial contribution
|
||||
* @contributer Christian Hoefler - Door sensor integration
|
||||
*/
|
||||
public class BridgeApiLockStateRequestDto {
|
||||
public class BridgeApiLockStateRequestDto extends BridgeApiDeviceStateDto {
|
||||
|
||||
private int nukiId;
|
||||
private int state;
|
||||
private String stateName;
|
||||
private boolean batteryCritical;
|
||||
private int doorsensorState;
|
||||
private Integer nukiId;
|
||||
|
||||
public int getNukiId() {
|
||||
public Integer getNukiId() {
|
||||
return nukiId;
|
||||
}
|
||||
|
||||
public void setNukiId(int nukiId) {
|
||||
public void setNukiId(Integer nukiId) {
|
||||
this.nukiId = nukiId;
|
||||
}
|
||||
|
||||
public int getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public String getStateName() {
|
||||
return stateName;
|
||||
}
|
||||
|
||||
public void setStateName(String stateName) {
|
||||
this.stateName = stateName;
|
||||
}
|
||||
|
||||
public boolean isBatteryCritical() {
|
||||
return batteryCritical;
|
||||
}
|
||||
|
||||
public void setBatteryCritical(boolean batteryCritical) {
|
||||
this.batteryCritical = batteryCritical;
|
||||
}
|
||||
|
||||
public int getDoorsensorState() {
|
||||
return doorsensorState;
|
||||
}
|
||||
|
||||
public void setDoorsensorState(int doorsensorState) {
|
||||
this.doorsensorState = doorsensorState;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* The {@link WebApiBridgeDiscoveryDto} class defines the Data Transfer Object (POJO) for a response of
|
||||
* the https://api.nuki.io/discover/bridges Web API.
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
public class WebApiBridgeDiscoveryDto {
|
||||
private List<WebApiBridgeDto> bridges;
|
||||
private Integer errorCode;
|
||||
|
||||
@NonNull
|
||||
public List<WebApiBridgeDto> getBridges() {
|
||||
if (bridges == null) {
|
||||
bridges = new ArrayList<>();
|
||||
}
|
||||
return bridges;
|
||||
}
|
||||
|
||||
public void setBridges(List<WebApiBridgeDto> bridges) {
|
||||
this.bridges = bridges;
|
||||
}
|
||||
|
||||
public Integer getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(Integer errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WebApiBridgeDiscoveryDto{" + "bridges=" + bridges + ", errorCode=" + errorCode + '}';
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.dto;
|
||||
|
||||
import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
||||
/**
|
||||
* The {@link WebApiBridgeDiscoveryDto} class defines the Data Transfer Object (POJO) for bridge object
|
||||
* the https://api.nuki.io/discover/bridges Web API.
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
public class WebApiBridgeDto {
|
||||
private String bridgeId;
|
||||
private String ip;
|
||||
private int port;
|
||||
private String dateUpdated;
|
||||
|
||||
public String getBridgeId() {
|
||||
return bridgeId;
|
||||
}
|
||||
|
||||
public void setBridgeId(String bridgeId) {
|
||||
this.bridgeId = bridgeId;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
public void setIp(String ip) {
|
||||
this.ip = ip;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getDateUpdated() {
|
||||
return dateUpdated;
|
||||
}
|
||||
|
||||
public void setDateUpdated(String dateUpdated) {
|
||||
this.dateUpdated = dateUpdated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WebApiBridgeDto{" + "bridgeId='" + bridgeId + '\'' + ", ip='" + ip + '\'' + ", port=" + port
|
||||
+ ", dateUpdated='" + dateUpdated + '\'' + '}';
|
||||
}
|
||||
|
||||
public ThingUID getThingUid() {
|
||||
return new ThingUID(NukiBindingConstants.THING_TYPE_BRIDGE, getBridgeId());
|
||||
}
|
||||
}
|
@ -0,0 +1,341 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.handler;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nuki.internal.configuration.NukiDeviceConfiguration;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.BridgeListResponse;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.BridgeLockStateResponse;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.NukiBaseResponse;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.NukiHttpClient;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiDeviceStateDto;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiListDeviceDto;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Channel;
|
||||
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.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.BridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AbstractNukiDeviceHandler} is a base class for implementing ThingHandlers for Nuki devices
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractNukiDeviceHandler<T extends NukiDeviceConfiguration> extends BaseThingHandler {
|
||||
|
||||
protected final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private static final int JOB_INTERVAL = 60;
|
||||
private static final Pattern NUKI_ID_HEX_PATTERN = Pattern.compile("[A-F\\d]{8}", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@Nullable
|
||||
protected ScheduledFuture<?> reInitJob;
|
||||
protected T configuration;
|
||||
@Nullable
|
||||
private NukiHttpClient nukiHttpClient;
|
||||
|
||||
private static String hexToDecimal(String hexString) {
|
||||
return String.valueOf(Integer.parseInt(hexString, 16));
|
||||
}
|
||||
|
||||
protected void withHttpClient(Consumer<NukiHttpClient> consumer) {
|
||||
withHttpClient(client -> {
|
||||
consumer.accept(client);
|
||||
return null;
|
||||
}, null);
|
||||
}
|
||||
|
||||
protected <U> U withHttpClient(Function<NukiHttpClient, U> consumer, U defaultValue) {
|
||||
NukiHttpClient client = this.nukiHttpClient;
|
||||
if (client == null) {
|
||||
logger.warn("Nuki HTTP client is null. This is a bug in Nuki Binding, please report it",
|
||||
new IllegalStateException());
|
||||
return defaultValue;
|
||||
} else {
|
||||
return consumer.apply(client);
|
||||
}
|
||||
}
|
||||
|
||||
public AbstractNukiDeviceHandler(Thing thing) {
|
||||
super(thing);
|
||||
this.configuration = getConfigAs(getConfigurationClass());
|
||||
// legacy support - check if nukiId is hexadecimal (which might have been set by previous binding version)
|
||||
// and convert it to decimal
|
||||
if (NUKI_ID_HEX_PATTERN.matcher(this.configuration.nukiId).matches()) {
|
||||
logger.warn(
|
||||
"SmartLock '{}' was created by old version of binding. It is recommended to delete it and discover again",
|
||||
thing.getUID());
|
||||
this.thing.getConfiguration().put(NukiBindingConstants.PROPERTY_NUKI_ID,
|
||||
hexToDecimal(configuration.nukiId));
|
||||
this.configuration = getConfigAs(getConfigurationClass());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
scheduler.execute(this::initializeHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopReInitJob();
|
||||
}
|
||||
|
||||
private void initializeHandler() {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
initializeHandler(null, null);
|
||||
} else {
|
||||
initializeHandler(bridge.getHandler(), bridge.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeHandler(@Nullable ThingHandler handler, @Nullable ThingStatus bridgeStatus) {
|
||||
if (handler instanceof NukiBridgeHandler && bridgeStatus != null) {
|
||||
NukiBridgeHandler bridgeHandler = (NukiBridgeHandler) handler;
|
||||
if (bridgeStatus == ThingStatus.ONLINE) {
|
||||
this.nukiHttpClient = bridgeHandler.getNukiHttpClient();
|
||||
withHttpClient(client -> {
|
||||
BridgeListResponse bridgeListResponse = client.getList();
|
||||
if (handleResponse(bridgeListResponse, null, null)) {
|
||||
BridgeApiListDeviceDto device = bridgeListResponse.getDevice(configuration.nukiId);
|
||||
if (device == null) {
|
||||
logger.warn("Configured Smart Lock [{}] not present in bridge device list",
|
||||
configuration.nukiId);
|
||||
} else {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
refreshData(device);
|
||||
stopReInitJob();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
stopReInitJob();
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
|
||||
stopReInitJob();
|
||||
}
|
||||
}
|
||||
|
||||
protected void refreshData(BridgeApiListDeviceDto device) {
|
||||
updateProperty(NukiBindingConstants.PROPERTY_NAME, device.getName());
|
||||
if (device.getFirmwareVersion() != null) {
|
||||
updateProperty(NukiBindingConstants.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion());
|
||||
}
|
||||
|
||||
if (device.getLastKnownState() != null) {
|
||||
refreshState(device.getLastKnownState());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to refresh state of this thing. Implementors should read values from state and update corresponding
|
||||
* channels.
|
||||
*
|
||||
* @param state Current state of this thing as obtained from Bridge API
|
||||
*/
|
||||
public abstract void refreshState(BridgeApiDeviceStateDto state);
|
||||
|
||||
protected <U> void updateState(String channelId, U state, Function<U, State> transform) {
|
||||
Channel channel = thing.getChannel(channelId);
|
||||
if (channel != null) {
|
||||
updateState(channel.getUID(), state, transform);
|
||||
}
|
||||
}
|
||||
|
||||
protected void triggerChannel(String channelId, String event) {
|
||||
Channel channel = thing.getChannel(channelId);
|
||||
if (channel != null) {
|
||||
triggerChannel(channel.getUID(), event);
|
||||
}
|
||||
}
|
||||
|
||||
protected <U> void updateState(ChannelUID channel, U state, Function<U, State> transform) {
|
||||
updateState(channel, state == null ? UnDefType.NULL : transform.apply(state));
|
||||
}
|
||||
|
||||
protected State toDateTime(String dateTimeString) {
|
||||
try {
|
||||
ZonedDateTime date = OffsetDateTime.parse(dateTimeString).atZoneSameInstant(ZoneId.systemDefault());
|
||||
return new DateTimeType(date);
|
||||
} catch (DateTimeParseException e) {
|
||||
logger.debug("Failed to parse date from '{}'", dateTimeString);
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
scheduler.execute(() -> {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
initializeHandler(null, bridgeStatusInfo.getStatus());
|
||||
} else {
|
||||
initializeHandler(bridge.getHandler(), bridgeStatusInfo.getStatus());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("handleCommand({}, {})", channelUID, command);
|
||||
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
logger.debug("Thing is not ONLINE; command[{}] for channelUID[{}] is ignored", command, channelUID);
|
||||
} else if (command instanceof RefreshType) {
|
||||
scheduler.execute(() -> {
|
||||
withHttpClient(client -> {
|
||||
BridgeLockStateResponse bridgeLockStateResponse = client.getBridgeLockState(configuration.nukiId,
|
||||
getDeviceType());
|
||||
if (handleResponse(bridgeLockStateResponse, channelUID.getAsString(), command.toString())) {
|
||||
if (!doHandleRefreshCommand(channelUID, command, bridgeLockStateResponse)) {
|
||||
logger.debug("Command[{}] for channelUID[{}] not implemented!", command, channelUID);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
scheduler.execute(() -> {
|
||||
if (!doHandleCommand(channelUID, command)) {
|
||||
logger.debug("Unexpected command[{}] for channelUID[{}]!", command, channelUID);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type of this device
|
||||
*
|
||||
* @return Device type
|
||||
*/
|
||||
protected abstract int getDeviceType();
|
||||
|
||||
/**
|
||||
* Get class of configuration
|
||||
*
|
||||
* @return Configuration class
|
||||
*/
|
||||
protected abstract Class<T> getConfigurationClass();
|
||||
|
||||
/**
|
||||
* Method to handle channel command - will not receive REFRESH command
|
||||
*
|
||||
* @param channelUID Channel which received command
|
||||
* @param command Command received
|
||||
* @return true if command was handled
|
||||
*/
|
||||
protected abstract boolean doHandleCommand(ChannelUID channelUID, Command command);
|
||||
|
||||
/**
|
||||
* Method for handlign {@link RefreshType} command
|
||||
*
|
||||
* @param channelUID Channel which received command
|
||||
* @param command Command received, will always be {@link RefreshType}
|
||||
* @param response Response from /lockState endpoint of Bridge API
|
||||
* @return true if command was handled
|
||||
*/
|
||||
protected boolean doHandleRefreshCommand(ChannelUID channelUID, Command command, BridgeLockStateResponse response) {
|
||||
refreshState(response.getBridgeApiLockStateDto());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean handleResponse(NukiBaseResponse nukiBaseResponse, @Nullable String channelUID,
|
||||
@Nullable String command) {
|
||||
if (nukiBaseResponse.getStatus() == 200 && nukiBaseResponse.isSuccess()) {
|
||||
logger.debug("Command[{}] succeeded for channelUID[{}] on nukiId[{}]!", command, channelUID,
|
||||
configuration.nukiId);
|
||||
return true;
|
||||
} else if (nukiBaseResponse.getStatus() != 200) {
|
||||
logger.debug("Request to Bridge failed! status[{}] - message[{}]", nukiBaseResponse.getStatus(),
|
||||
nukiBaseResponse.getMessage());
|
||||
} else if (!nukiBaseResponse.isSuccess()) {
|
||||
logger.debug(
|
||||
"Request from Bridge to Smart Lock failed! status[{}] - message[{}] - isSuccess[{}]. Check if Nuki Smart Lock is powered on!",
|
||||
nukiBaseResponse.getStatus(), nukiBaseResponse.getMessage(), nukiBaseResponse.isSuccess());
|
||||
}
|
||||
logger.debug("Could not handle command[{}] for channelUID[{}] on nukiId[{}]!", command, channelUID,
|
||||
configuration.nukiId);
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, nukiBaseResponse.getMessage());
|
||||
|
||||
updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_LOCK, OnOffType.OFF, Function.identity());
|
||||
updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_STATE, NukiBindingConstants.LOCK_STATES_UNDEFINED,
|
||||
DecimalType::new);
|
||||
updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_DOOR_STATE, NukiBindingConstants.DOORSENSOR_STATES_UNKNOWN,
|
||||
DecimalType::new);
|
||||
|
||||
withBridgeAsync(bridge -> {
|
||||
bridge.checkBridgeOnline();
|
||||
startReInitJob();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
private void withBridgeAsync(Consumer<NukiBridgeHandler> handler) {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
BridgeHandler bridgeHandler = bridge.getHandler();
|
||||
if (bridgeHandler instanceof NukiBridgeHandler) {
|
||||
scheduler.execute(() -> handler.accept((NukiBridgeHandler) bridgeHandler));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startReInitJob() {
|
||||
logger.trace("Starting reInitJob with interval of {}secs for Smart Lock[{}].", JOB_INTERVAL,
|
||||
configuration.nukiId);
|
||||
if (reInitJob != null) {
|
||||
logger.trace("Already started reInitJob for Smart Lock[{}].", configuration.nukiId);
|
||||
return;
|
||||
}
|
||||
reInitJob = scheduler.scheduleWithFixedDelay(this::initializeHandler, 1, JOB_INTERVAL, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void stopReInitJob() {
|
||||
logger.trace("Stopping reInitJob for Smart Lock[{}].", configuration.nukiId);
|
||||
ScheduledFuture<?> job = reInitJob;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
logger.trace("Stopped reInitJob for Smart Lock[{}].", configuration.nukiId);
|
||||
}
|
||||
reInitJob = null;
|
||||
}
|
||||
}
|
@ -12,25 +12,37 @@
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.handler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.nuki.internal.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.configuration.NukiBridgeConfiguration;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiLinkBuilder;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.BridgeCallbackAddResponse;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.BridgeCallbackListResponse;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.BridgeCallbackRemoveResponse;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.BridgeInfoResponse;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.NukiHttpClient;
|
||||
import org.openhab.binding.nuki.internal.discovery.NukiDeviceDiscoveryService;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiCallbackListCallbackDto;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -40,55 +52,70 @@ import org.slf4j.LoggerFactory;
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Markus Katter - Initial contribution
|
||||
* @contributer Jan Vybíral - Improved callback handling
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NukiBridgeHandler extends BaseBridgeHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NukiBridgeHandler.class);
|
||||
private static final int JOB_INTERVAL = 600;
|
||||
|
||||
private HttpClient httpClient;
|
||||
private final HttpClient httpClient;
|
||||
@Nullable
|
||||
private NukiHttpClient nukiHttpClient;
|
||||
private String callbackUrl;
|
||||
@Nullable
|
||||
private final String callbackUrl;
|
||||
@Nullable
|
||||
private ScheduledFuture<?> checkBridgeOnlineJob;
|
||||
private String bridgeIp;
|
||||
private boolean manageCallbacks;
|
||||
private boolean initializable;
|
||||
private NukiBridgeConfiguration config = new NukiBridgeConfiguration();
|
||||
|
||||
public NukiBridgeHandler(Bridge bridge, HttpClient httpClient, String callbackUrl) {
|
||||
public NukiBridgeHandler(Bridge bridge, HttpClient httpClient, @Nullable String callbackUrl) {
|
||||
super(bridge);
|
||||
logger.debug("Instantiating NukiBridgeHandler({}, {}, {})", bridge, httpClient, callbackUrl);
|
||||
this.httpClient = httpClient;
|
||||
this.callbackUrl = callbackUrl;
|
||||
this.initializable = getConfig().get(NukiBindingConstants.CONFIG_IP) != null
|
||||
&& getConfig().get(NukiBindingConstants.CONFIG_API_TOKEN) != null;
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
public NukiHttpClient getNukiHttpClient() {
|
||||
if (nukiHttpClient == null) {
|
||||
nukiHttpClient = new NukiHttpClient(httpClient, getConfig());
|
||||
public @Nullable NukiHttpClient getNukiHttpClient() {
|
||||
return this.nukiHttpClient;
|
||||
}
|
||||
|
||||
public void withHttpClient(Consumer<NukiHttpClient> consumer) {
|
||||
withHttpClient(client -> {
|
||||
consumer.accept(client);
|
||||
return null;
|
||||
}, null);
|
||||
}
|
||||
|
||||
protected <@Nullable U> @Nullable U withHttpClient(Function<NukiHttpClient, U> consumer, U defaultValue) {
|
||||
NukiHttpClient client = this.nukiHttpClient;
|
||||
if (client == null) {
|
||||
logger.warn("Nuki HTTP client is null. This is a bug in Nuki Binding, please report it",
|
||||
new IllegalStateException());
|
||||
return defaultValue;
|
||||
} else {
|
||||
return consumer.apply(client);
|
||||
}
|
||||
return nukiHttpClient;
|
||||
}
|
||||
|
||||
public boolean isInitializable() {
|
||||
return initializable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("initialize() for Bridge[{}].", getThing().getUID());
|
||||
Configuration config = getConfig();
|
||||
bridgeIp = (String) config.get(NukiBindingConstants.CONFIG_IP);
|
||||
manageCallbacks = (Boolean) config.get(NukiBindingConstants.CONFIG_MANAGECB);
|
||||
if (bridgeIp == null) {
|
||||
this.config = getConfigAs(NukiBridgeConfiguration.class);
|
||||
String ip = config.ip;
|
||||
Integer port = config.port;
|
||||
String apiToken = config.apiToken;
|
||||
|
||||
if (ip == null || port == null) {
|
||||
logger.debug("NukiBridgeHandler[{}] is not initializable, IP setting is unset in the configuration!",
|
||||
getThing().getUID());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP setting is unset");
|
||||
} else if (config.get(NukiBindingConstants.CONFIG_API_TOKEN) == null) {
|
||||
} else if (apiToken == null || apiToken.isBlank()) {
|
||||
logger.debug("NukiBridgeHandler[{}] is not initializable, apiToken setting is unset in the configuration!",
|
||||
getThing().getUID());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "apiToken setting is unset");
|
||||
} else {
|
||||
NukiLinkBuilder linkBuilder = new NukiLinkBuilder(ip, port, apiToken, this.config.secureToken);
|
||||
nukiHttpClient = new NukiHttpClient(httpClient, linkBuilder);
|
||||
scheduler.execute(this::initializeHandler);
|
||||
checkBridgeOnlineJob = scheduler.scheduleWithFixedDelay(this::checkBridgeOnline, JOB_INTERVAL, JOB_INTERVAL,
|
||||
TimeUnit.SECONDS);
|
||||
@ -97,98 +124,169 @@ public class NukiBridgeHandler extends BaseBridgeHandler {
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.debug("handleCommand({}, {}) for Bridge[{}] not implemented!", channelUID, command, bridgeIp);
|
||||
logger.debug("handleCommand({}, {}) for Bridge[{}] not implemented!", channelUID, command, this.config.ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("dispose() for Bridge[{}].", getThing().getUID());
|
||||
if (this.config.manageCallbacks) {
|
||||
unregisterCallback();
|
||||
}
|
||||
nukiHttpClient = null;
|
||||
if (checkBridgeOnlineJob != null && !checkBridgeOnlineJob.isCancelled()) {
|
||||
checkBridgeOnlineJob.cancel(true);
|
||||
ScheduledFuture<?> job = checkBridgeOnlineJob;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
checkBridgeOnlineJob = null;
|
||||
}
|
||||
|
||||
private synchronized void initializeHandler() {
|
||||
logger.debug("initializeHandler() for Bridge[{}].", bridgeIp);
|
||||
BridgeInfoResponse bridgeInfoResponse = getNukiHttpClient().getBridgeInfo();
|
||||
if (bridgeInfoResponse.getStatus() == HttpStatus.OK_200) {
|
||||
if (manageCallbacks) {
|
||||
manageNukiBridgeCallbacks();
|
||||
}
|
||||
logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge online.", bridgeIp,
|
||||
bridgeInfoResponse.getStatus());
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge offline!", bridgeIp,
|
||||
bridgeInfoResponse.getStatus());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, bridgeInfoResponse.getMessage());
|
||||
}
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Collections.singleton(NukiDeviceDiscoveryService.class);
|
||||
}
|
||||
|
||||
private void checkBridgeOnline() {
|
||||
logger.debug("checkBridgeOnline():bridgeIp[{}] status[{}]", bridgeIp, getThing().getStatus());
|
||||
if (getThing().getStatus().equals(ThingStatus.ONLINE)) {
|
||||
logger.debug("Requesting BridgeInfo to ensure Bridge[{}] is online.", bridgeIp);
|
||||
BridgeInfoResponse bridgeInfoResponse = getNukiHttpClient().getBridgeInfo();
|
||||
int status = bridgeInfoResponse.getStatus();
|
||||
if (status == HttpStatus.OK_200) {
|
||||
logger.debug("Bridge[{}] responded with status[{}]. Bridge is online.", bridgeIp, status);
|
||||
} else if (status == HttpStatus.SERVICE_UNAVAILABLE_503) {
|
||||
logger.debug(
|
||||
"Bridge[{}] responded with status[{}]. REST service seems to be busy but Bridge is online.",
|
||||
bridgeIp, status);
|
||||
private synchronized void initializeHandler() {
|
||||
withHttpClient(client -> {
|
||||
BridgeInfoResponse bridgeInfoResponse = client.getBridgeInfo();
|
||||
if (bridgeInfoResponse.getStatus() == HttpStatus.OK_200) {
|
||||
updateProperty(NukiBindingConstants.PROPERTY_FIRMWARE_VERSION, bridgeInfoResponse.getFirmwareVersion());
|
||||
updateProperty(NukiBindingConstants.PROPERTY_WIFI_FIRMWARE_VERSION,
|
||||
bridgeInfoResponse.getWifiFirmwareVersion());
|
||||
updateProperty(NukiBindingConstants.PROPERTY_HARDWARE_ID,
|
||||
Integer.toString(bridgeInfoResponse.getHardwareId()));
|
||||
updateProperty(NukiBindingConstants.PROPERTY_SERVER_ID,
|
||||
Integer.toString(bridgeInfoResponse.getServerId()));
|
||||
if (this.config.manageCallbacks) {
|
||||
manageNukiBridgeCallbacks();
|
||||
}
|
||||
logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge online.", this.config.ip,
|
||||
bridgeInfoResponse.getStatus());
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge offline!", bridgeIp, status);
|
||||
logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge offline!", this.config.ip,
|
||||
bridgeInfoResponse.getStatus());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
bridgeInfoResponse.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void checkBridgeOnline() {
|
||||
logger.debug("checkBridgeOnline():bridgeIp[{}] status[{}]", this.config.ip, getThing().getStatus());
|
||||
if (getThing().getStatus().equals(ThingStatus.ONLINE)) {
|
||||
|
||||
withHttpClient(client -> {
|
||||
logger.debug("Requesting BridgeInfo to ensure Bridge[{}] is online.", this.config.ip);
|
||||
BridgeInfoResponse bridgeInfoResponse = client.getBridgeInfo();
|
||||
int status = bridgeInfoResponse.getStatus();
|
||||
if (status == HttpStatus.OK_200) {
|
||||
logger.debug("Bridge[{}] responded with status[{}]. Bridge is online.", this.config.ip, status);
|
||||
} else if (status == HttpStatus.SERVICE_UNAVAILABLE_503) {
|
||||
logger.debug(
|
||||
"Bridge[{}] responded with status[{}]. REST service seems to be busy but Bridge is online.",
|
||||
this.config.ip, status);
|
||||
} else {
|
||||
logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge offline!", this.config.ip,
|
||||
status);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
bridgeInfoResponse.getMessage());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
initializeHandler();
|
||||
}
|
||||
}
|
||||
|
||||
private void manageNukiBridgeCallbacks() {
|
||||
logger.debug("manageNukiBridgeCallbacks() for Bridge[{}].", bridgeIp);
|
||||
BridgeCallbackListResponse bridgeCallbackListResponse = getNukiHttpClient().getBridgeCallbackList();
|
||||
List<BridgeApiCallbackListCallbackDto> callbacks = bridgeCallbackListResponse.getCallbacks();
|
||||
boolean callbackExists = false;
|
||||
int callbackCount = callbacks == null ? 0 : callbacks.size();
|
||||
if (callbacks != null) {
|
||||
for (BridgeApiCallbackListCallbackDto callback : callbacks) {
|
||||
if (callback.getUrl().equals(callbackUrl)) {
|
||||
logger.debug("callbackUrl[{}] already existing on Bridge[{}].", callbackUrl, bridgeIp);
|
||||
callbackExists = true;
|
||||
continue;
|
||||
}
|
||||
if (callback.getUrl().contains(NukiBindingConstants.CALLBACK_ENDPOINT)) {
|
||||
logger.debug("Partial callbackUrl[{}] found on Bridge[{}] - Removing it!", callbackUrl, bridgeIp);
|
||||
BridgeCallbackRemoveResponse bridgeCallbackRemoveResponse = getNukiHttpClient()
|
||||
.getBridgeCallbackRemove(callback.getId());
|
||||
if (bridgeCallbackRemoveResponse.getStatus() == HttpStatus.OK_200) {
|
||||
logger.debug("Successfully removed callbackUrl[{}] on Bridge[{}]!", callbackUrl, bridgeIp);
|
||||
callbackCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!callbackExists) {
|
||||
if (callbackCount == 3) {
|
||||
logger.debug("Already 3 callback URLs existing on Bridge[{}] - Removing ID 0!", bridgeIp);
|
||||
BridgeCallbackRemoveResponse bridgeCallbackRemoveResponse = getNukiHttpClient()
|
||||
.getBridgeCallbackRemove(0);
|
||||
if (bridgeCallbackRemoveResponse.getStatus() == HttpStatus.OK_200) {
|
||||
logger.debug("Successfully removed callbackUrl[{}] on Bridge[{}]!", callbackUrl, bridgeIp);
|
||||
callbackCount--;
|
||||
}
|
||||
}
|
||||
logger.debug("Adding callbackUrl[{}] to Bridge[{}]!", callbackUrl, bridgeIp);
|
||||
BridgeCallbackAddResponse bridgeCallbackAddResponse = getNukiHttpClient().getBridgeCallbackAdd(callbackUrl);
|
||||
if (bridgeCallbackAddResponse.getStatus() == HttpStatus.OK_200) {
|
||||
logger.debug("Successfully added callbackUrl[{}] on Bridge[{}]!", callbackUrl, bridgeIp);
|
||||
callbackExists = true;
|
||||
}
|
||||
private boolean isHttpClientNull() {
|
||||
NukiHttpClient httpClient = getNukiHttpClient();
|
||||
if (httpClient == null) {
|
||||
logger.debug("HTTP Client not configured, switching bridge to OFFLINE");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "HTTP Client not configured");
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable List<BridgeApiCallbackListCallbackDto> listCallbacks() {
|
||||
if (isHttpClientNull()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return withHttpClient(client -> {
|
||||
BridgeCallbackListResponse bridgeCallbackListResponse = client.getBridgeCallbackList();
|
||||
if (bridgeCallbackListResponse.isSuccess()) {
|
||||
return bridgeCallbackListResponse.getCallbacks();
|
||||
} else {
|
||||
logger.debug("Failed to list callbacks for Bridge[{}] - status {}, message {}", this.config.ip,
|
||||
bridgeCallbackListResponse.getStatus(), bridgeCallbackListResponse.getMessage());
|
||||
return null;
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
private void manageNukiBridgeCallbacks() {
|
||||
String callback = callbackUrl;
|
||||
if (callback == null) {
|
||||
logger.debug("Cannot manage callbacks - no URL available");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("manageNukiBridgeCallbacks() for Bridge[{}].", this.config.ip);
|
||||
|
||||
List<BridgeApiCallbackListCallbackDto> callbacks = listCallbacks();
|
||||
if (callbacks == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Integer> callbacksToRemove = new ArrayList<>(3);
|
||||
|
||||
// callback already registered - do nothing
|
||||
if (callbacks.stream().anyMatch(cb -> cb.getUrl().equals(callback))) {
|
||||
logger.debug("callbackUrl[{}] already existing on Bridge[{}].", callbackUrl, this.config.ip);
|
||||
return;
|
||||
}
|
||||
// delete callbacks for this bridge registered for different host
|
||||
String path = NukiLinkBuilder.callbackPath(getThing().getUID().getId()).build().toString();
|
||||
callbacks.stream().filter(cb -> cb.getUrl().endsWith(path)).map(BridgeApiCallbackListCallbackDto::getId)
|
||||
.forEach(callbacksToRemove::add);
|
||||
// delete callbacks for this bridge registered without bridgeId query (created by previous binding version)
|
||||
String urlWithoutQuery = UriBuilder.fromUri(callback).replaceQuery("").build().toString();
|
||||
callbacks.stream().filter(cb -> cb.getUrl().equals(urlWithoutQuery))
|
||||
.map(BridgeApiCallbackListCallbackDto::getId).forEach(callbacksToRemove::add);
|
||||
|
||||
if (callbacks.size() - callbacksToRemove.size() == 3) {
|
||||
logger.debug("Already 3 callback URLs existing on Bridge[{}] - Removing ID 0!", this.config.ip);
|
||||
callbacksToRemove.add(0);
|
||||
}
|
||||
|
||||
callbacksToRemove.forEach(callbackId -> {
|
||||
withHttpClient(client -> {
|
||||
BridgeCallbackRemoveResponse bridgeCallbackRemoveResponse = client.getBridgeCallbackRemove(callbackId);
|
||||
if (bridgeCallbackRemoveResponse.getStatus() == HttpStatus.OK_200) {
|
||||
logger.debug("Successfully removed callbackUrl[{}] on Bridge[{}]!", callback, this.config.ip);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
withHttpClient(client -> {
|
||||
logger.debug("Adding callbackUrl[{}] to Bridge[{}]!", callbackUrl, this.config.ip);
|
||||
BridgeCallbackAddResponse bridgeCallbackAddResponse = client.getBridgeCallbackAdd(callback);
|
||||
if (bridgeCallbackAddResponse.getStatus() == HttpStatus.OK_200) {
|
||||
logger.debug("Successfully added callbackUrl[{}] on Bridge[{}]!", callback, this.config.ip);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void unregisterCallback() {
|
||||
List<BridgeApiCallbackListCallbackDto> callbacks = listCallbacks();
|
||||
if (callbacks == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
callbacks.stream().filter(callback -> callback.getUrl().equals(callbackUrl))
|
||||
.map(BridgeApiCallbackListCallbackDto::getId)
|
||||
.forEach(callbackId -> withHttpClient(client -> client.getBridgeCallbackRemove(callbackId)));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.handler;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.nuki.internal.configuration.NukiDeviceConfiguration;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.constants.OpenerAction;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.BridgeLockActionResponse;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiDeviceStateDto;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* Thing handler for Nuki Opener
|
||||
*
|
||||
* @author Jan Vybíral - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NukiOpenerHandler extends AbstractNukiDeviceHandler<NukiDeviceConfiguration> {
|
||||
|
||||
public NukiOpenerHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
private volatile Instant lastRingAction = Instant.EPOCH;
|
||||
|
||||
@Override
|
||||
public void refreshState(BridgeApiDeviceStateDto state) {
|
||||
updateState(NukiBindingConstants.CHANNEL_OPENER_LOW_BATTERY, state.isBatteryCritical(), OnOffType::from);
|
||||
updateState(NukiBindingConstants.CHANNEL_OPENER_STATE, state.getState(), DecimalType::new);
|
||||
updateState(NukiBindingConstants.CHANNEL_OPENER_MODE, state.getMode(), DecimalType::new);
|
||||
updateState(NukiBindingConstants.CHANNEL_OPENER_RING_ACTION_TIMESTAMP, state.getRingactionTimestamp(),
|
||||
this::toDateTime);
|
||||
|
||||
if (state.getRingactionState() && Duration.between(lastRingAction, Instant.now()).getSeconds() > 30) {
|
||||
triggerChannel(NukiBindingConstants.CHANNEL_OPENER_RING_ACTION_STATE, NukiBindingConstants.EVENT_RINGING);
|
||||
lastRingAction = Instant.now();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDeviceType() {
|
||||
return NukiBindingConstants.DEVICE_OPENER;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doHandleCommand(ChannelUID channelUID, Command command) {
|
||||
switch (channelUID.getId()) {
|
||||
case NukiBindingConstants.CHANNEL_OPENER_STATE:
|
||||
if (command instanceof DecimalType) {
|
||||
OpenerAction action = OpenerAction.fromAction(((DecimalType) command).intValue());
|
||||
if (action != null) {
|
||||
return withHttpClient(client -> {
|
||||
BridgeLockActionResponse response = client.getOpenerAction(configuration.nukiId, action);
|
||||
return handleResponse(response, channelUID.getAsString(), command.toString());
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<NukiDeviceConfiguration> getConfigurationClass() {
|
||||
return NukiDeviceConfiguration.class;
|
||||
}
|
||||
}
|
@ -12,32 +12,17 @@
|
||||
*/
|
||||
package org.openhab.binding.nuki.internal.handler;
|
||||
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.openhab.binding.nuki.internal.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.converter.LockActionConverter;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.nuki.internal.configuration.NukiSmartLockConfiguration;
|
||||
import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
|
||||
import org.openhab.binding.nuki.internal.constants.SmartLockAction;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.BridgeLockActionResponse;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.BridgeLockStateResponse;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.NukiBaseResponse;
|
||||
import org.openhab.binding.nuki.internal.dataexchange.NukiHttpClient;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.binding.nuki.internal.dto.BridgeApiDeviceStateDto;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Channel;
|
||||
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.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link NukiSmartLockHandler} is responsible for handling commands, which are
|
||||
@ -45,245 +30,81 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* @author Markus Katter - Initial contribution
|
||||
* @contributer Christian Hoefler - Door sensor integration
|
||||
* @contributer Jan Vybíral - Refactoring, added more channels
|
||||
*/
|
||||
public class NukiSmartLockHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NukiSmartLockHandler.class);
|
||||
private static final int JOB_INTERVAL = 60;
|
||||
|
||||
private NukiHttpClient nukiHttpClient;
|
||||
private ScheduledFuture<?> reInitJob;
|
||||
private String nukiId;
|
||||
private boolean unlatch;
|
||||
@NonNullByDefault
|
||||
public class NukiSmartLockHandler extends AbstractNukiDeviceHandler<NukiSmartLockConfiguration> {
|
||||
|
||||
public NukiSmartLockHandler(Thing thing) {
|
||||
super(thing);
|
||||
logger.debug("Instantiating NukiSmartLockHandler({})", thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("initialize() for Smart Lock[{}].", getThing().getUID());
|
||||
Configuration config = getConfig();
|
||||
nukiId = (String) config.get(NukiBindingConstants.CONFIG_NUKI_ID);
|
||||
unlatch = (Boolean) config.get(NukiBindingConstants.CONFIG_UNLATCH);
|
||||
if (nukiId == null) {
|
||||
logger.debug("NukiSmartLockHandler[{}] is not initializable, nukiId setting is unset in the configuration!",
|
||||
getThing().getUID());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "nukiId setting is unset");
|
||||
} else {
|
||||
scheduler.execute(this::initializeHandler);
|
||||
}
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("dispose() for Smart Lock[{}].", getThing().getUID());
|
||||
stopReInitJob();
|
||||
}
|
||||
|
||||
private void initializeHandler() {
|
||||
logger.debug("initializeHandler() for Smart Lock[{}]", nukiId);
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
initializeHandler(null, null);
|
||||
} else {
|
||||
initializeHandler(bridge.getHandler(), bridge.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeHandler(ThingHandler bridgeHandler, ThingStatus bridgeStatus) {
|
||||
if (bridgeHandler != null && bridgeStatus != null) {
|
||||
if (bridgeStatus == ThingStatus.ONLINE) {
|
||||
nukiHttpClient = ((NukiBridgeHandler) bridgeHandler).getNukiHttpClient();
|
||||
BridgeLockStateResponse bridgeLockStateResponse = nukiHttpClient.getBridgeLockState(nukiId);
|
||||
if (handleResponse(bridgeLockStateResponse, null, null)) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
for (Channel channel : thing.getChannels()) {
|
||||
handleCommand(channel.getUID(), RefreshType.REFRESH);
|
||||
}
|
||||
stopReInitJob();
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
stopReInitJob();
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
|
||||
stopReInitJob();
|
||||
}
|
||||
public void refreshState(BridgeApiDeviceStateDto state) {
|
||||
updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_LOCK,
|
||||
state.getState() == NukiBindingConstants.LOCK_STATES_LOCKED, OnOffType::from);
|
||||
updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_BATTERY_LEVEL, state.getBatteryChargeState(),
|
||||
DecimalType::new);
|
||||
updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_BATTERY_CHARGING, state.getBatteryCharging(),
|
||||
OnOffType::from);
|
||||
updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_LOW_BATTERY, state.isBatteryCritical(), OnOffType::from);
|
||||
updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_KEYPAD_LOW_BATTERY, state.getKeypadBatteryCritical(),
|
||||
OnOffType::from);
|
||||
updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_STATE, state.getState(), DecimalType::new);
|
||||
updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_DOOR_STATE, state.getDoorsensorState(), DecimalType::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
logger.debug("bridgeStatusChanged({}) for Smart Lock[{}].", bridgeStatusInfo, nukiId);
|
||||
scheduler.execute(() -> {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
initializeHandler(null, bridgeStatusInfo.getStatus());
|
||||
} else {
|
||||
initializeHandler(bridge.getHandler(), bridgeStatusInfo.getStatus());
|
||||
}
|
||||
});
|
||||
protected int getDeviceType() {
|
||||
return NukiBindingConstants.DEVICE_SMART_LOCK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.debug("handleCommand({}, {})", channelUID, command);
|
||||
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
logger.debug("Thing is not ONLINE; command[{}] for channelUID[{}] is ignored", command, channelUID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
handleCommandRefreshType(channelUID, command);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean validCmd = true;
|
||||
protected boolean doHandleCommand(ChannelUID channelUID, Command command) {
|
||||
switch (channelUID.getId()) {
|
||||
case NukiBindingConstants.CHANNEL_SMARTLOCK_LOCK:
|
||||
if (command instanceof OnOffType) {
|
||||
int lockAction;
|
||||
if (unlatch) {
|
||||
lockAction = (command == OnOffType.OFF ? NukiBindingConstants.LOCK_ACTIONS_UNLATCH
|
||||
: NukiBindingConstants.LOCK_ACTIONS_LOCK);
|
||||
final SmartLockAction action;
|
||||
|
||||
if (command == OnOffType.OFF) {
|
||||
action = configuration.unlatch ? SmartLockAction.UNLATCH : SmartLockAction.UNLOCK;
|
||||
} else {
|
||||
lockAction = (command == OnOffType.OFF ? NukiBindingConstants.LOCK_ACTIONS_UNLOCK
|
||||
: NukiBindingConstants.LOCK_ACTIONS_LOCK);
|
||||
action = SmartLockAction.LOCK;
|
||||
}
|
||||
Channel channelLockState = thing.getChannel(NukiBindingConstants.CHANNEL_SMARTLOCK_STATE);
|
||||
if (channelLockState != null) {
|
||||
updateState(channelLockState.getUID(),
|
||||
new DecimalType(LockActionConverter.getLockStateFor(lockAction)));
|
||||
}
|
||||
BridgeLockActionResponse bridgeLockActionResponse = nukiHttpClient.getBridgeLockAction(nukiId,
|
||||
lockAction);
|
||||
handleResponse(bridgeLockActionResponse, channelUID.getAsString(), command.toString());
|
||||
} else {
|
||||
validCmd = false;
|
||||
|
||||
withHttpClient(client -> {
|
||||
BridgeLockActionResponse bridgeLockActionResponse = client
|
||||
.getSmartLockAction(configuration.nukiId, action);
|
||||
handleResponse(bridgeLockActionResponse, channelUID.getAsString(), command.toString());
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case NukiBindingConstants.CHANNEL_SMARTLOCK_STATE:
|
||||
if (command instanceof DecimalType) {
|
||||
int lockAction;
|
||||
lockAction = ((DecimalType) command).intValue();
|
||||
lockAction = LockActionConverter.getLockActionFor(lockAction);
|
||||
updateState(channelUID, new DecimalType(LockActionConverter.getLockStateFor(lockAction)));
|
||||
BridgeLockActionResponse bridgeLockActionResponse = nukiHttpClient.getBridgeLockAction(nukiId,
|
||||
lockAction);
|
||||
handleResponse(bridgeLockActionResponse, channelUID.getAsString(), command.toString());
|
||||
} else {
|
||||
validCmd = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
validCmd = false;
|
||||
break;
|
||||
}
|
||||
if (!validCmd) {
|
||||
logger.debug("Unexpected command[{}] for channelUID[{}]!", command, channelUID);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCommandRefreshType(ChannelUID channelUID, Command command) {
|
||||
logger.debug("handleCommandRefreshType({}, {})", channelUID, command);
|
||||
BridgeLockStateResponse bridgeLockStateResponse;
|
||||
switch (channelUID.getId()) {
|
||||
case NukiBindingConstants.CHANNEL_SMARTLOCK_LOCK:
|
||||
bridgeLockStateResponse = nukiHttpClient.getBridgeLockState(nukiId);
|
||||
if (handleResponse(bridgeLockStateResponse, channelUID.getAsString(), command.toString())) {
|
||||
int lockState = bridgeLockStateResponse.getState();
|
||||
State state;
|
||||
if (lockState == NukiBindingConstants.LOCK_STATES_LOCKED) {
|
||||
state = OnOffType.ON;
|
||||
} else if (lockState == NukiBindingConstants.LOCK_STATES_UNLOCKED) {
|
||||
state = OnOffType.OFF;
|
||||
} else {
|
||||
logger.warn(
|
||||
"Smart Lock returned lockState[{}]. Intentionally setting possibly wrong value 'OFF' for channel 'smartlockLock'!",
|
||||
lockState);
|
||||
state = OnOffType.OFF;
|
||||
DecimalType cmd = (DecimalType) command;
|
||||
SmartLockAction action = SmartLockAction.fromAction(cmd.intValue());
|
||||
if (action != null) {
|
||||
withHttpClient(client -> {
|
||||
BridgeLockActionResponse bridgeLockActionResponse = client
|
||||
.getSmartLockAction(configuration.nukiId, action);
|
||||
handleResponse(bridgeLockActionResponse, channelUID.getAsString(), command.toString());
|
||||
});
|
||||
}
|
||||
updateState(channelUID, state);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case NukiBindingConstants.CHANNEL_SMARTLOCK_STATE:
|
||||
bridgeLockStateResponse = nukiHttpClient.getBridgeLockState(nukiId);
|
||||
if (handleResponse(bridgeLockStateResponse, channelUID.getAsString(), command.toString())) {
|
||||
updateState(channelUID, new DecimalType(bridgeLockStateResponse.getState()));
|
||||
}
|
||||
break;
|
||||
case NukiBindingConstants.CHANNEL_SMARTLOCK_LOW_BATTERY:
|
||||
bridgeLockStateResponse = nukiHttpClient.getBridgeLockState(nukiId);
|
||||
if (handleResponse(bridgeLockStateResponse, channelUID.getAsString(), command.toString())) {
|
||||
updateState(channelUID, bridgeLockStateResponse.isBatteryCritical() ? OnOffType.ON : OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case NukiBindingConstants.CHANNEL_SMARTLOCK_DOOR_STATE:
|
||||
bridgeLockStateResponse = nukiHttpClient.getBridgeLockState(nukiId);
|
||||
if (handleResponse(bridgeLockStateResponse, channelUID.getAsString(), command.toString())) {
|
||||
updateState(channelUID, new DecimalType(bridgeLockStateResponse.getDoorsensorState()));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.debug("Command[{}] for channelUID[{}] not implemented!", command, channelUID);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleResponse(NukiBaseResponse nukiBaseResponse, String channelUID, String command) {
|
||||
if (nukiBaseResponse.getStatus() == 200 && nukiBaseResponse.isSuccess()) {
|
||||
logger.debug("Command[{}] succeeded for channelUID[{}] on nukiId[{}]!", command, channelUID, nukiId);
|
||||
return true;
|
||||
} else if (nukiBaseResponse.getStatus() != 200) {
|
||||
logger.debug("Request to Bridge failed! status[{}] - message[{}]", nukiBaseResponse.getStatus(),
|
||||
nukiBaseResponse.getMessage());
|
||||
} else if (!nukiBaseResponse.isSuccess()) {
|
||||
logger.debug(
|
||||
"Request from Bridge to Smart Lock failed! status[{}] - message[{}] - isSuccess[{}]. Check if Nuki Smart Lock is powered on!",
|
||||
nukiBaseResponse.getStatus(), nukiBaseResponse.getMessage(), nukiBaseResponse.isSuccess());
|
||||
}
|
||||
logger.debug("Could not handle command[{}] for channelUID[{}] on nukiId[{}]!", command, channelUID, nukiId);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, nukiBaseResponse.getMessage());
|
||||
Channel channelLock = thing.getChannel(NukiBindingConstants.CHANNEL_SMARTLOCK_LOCK);
|
||||
if (channelLock != null) {
|
||||
updateState(channelLock.getUID(), OnOffType.OFF);
|
||||
}
|
||||
Channel channelLockState = thing.getChannel(NukiBindingConstants.CHANNEL_SMARTLOCK_STATE);
|
||||
if (channelLockState != null) {
|
||||
updateState(channelLockState.getUID(), new DecimalType(NukiBindingConstants.LOCK_STATES_UNDEFINED));
|
||||
}
|
||||
Channel channelDoorState = thing.getChannel(NukiBindingConstants.CHANNEL_SMARTLOCK_DOOR_STATE);
|
||||
if (channelDoorState != null) {
|
||||
updateState(channelDoorState.getUID(), new DecimalType(NukiBindingConstants.DOORSENSOR_STATES_UNKNOWN));
|
||||
}
|
||||
startReInitJob();
|
||||
return false;
|
||||
}
|
||||
|
||||
private void startReInitJob() {
|
||||
logger.trace("Starting reInitJob with interval of {}secs for Smart Lock[{}].", JOB_INTERVAL, nukiId);
|
||||
if (reInitJob != null) {
|
||||
logger.trace("Already started reInitJob for Smart Lock[{}].", nukiId);
|
||||
return;
|
||||
}
|
||||
reInitJob = scheduler.scheduleWithFixedDelay(this::initializeHandler, JOB_INTERVAL, JOB_INTERVAL,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void stopReInitJob() {
|
||||
logger.trace("Stopping reInitJob for Smart Lock[{}].", nukiId);
|
||||
if (reInitJob != null && !reInitJob.isCancelled()) {
|
||||
logger.trace("Stopped reInitJob for Smart Lock[{}].", nukiId);
|
||||
reInitJob.cancel(true);
|
||||
}
|
||||
reInitJob = null;
|
||||
}
|
||||
|
||||
public void handleApiServletUpdate(ChannelUID channelUID, State newState) {
|
||||
logger.trace("handleApiServletUpdate({}, {})", channelUID, newState);
|
||||
updateState(channelUID, newState);
|
||||
@Override
|
||||
protected Class<NukiSmartLockConfiguration> getConfigurationClass() {
|
||||
return NukiSmartLockConfiguration.class;
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,13 @@
|
||||
<label>Nuki Bridge</label>
|
||||
<description>This bridge represents a Nuki Bridge on your local network. Nuki Smart Locks have to be paired via
|
||||
Bluetooth with it.</description>
|
||||
<properties>
|
||||
<property name="bridgeId"/>
|
||||
<property name="hardwareId"/>
|
||||
<property name="serverId"/>
|
||||
<property name="firmwareVersion"/>
|
||||
<property name="wifiFirmwareVersion"/>
|
||||
</properties>
|
||||
<config-description>
|
||||
<parameter name="ip" type="text" required="true">
|
||||
<label>IP Address</label>
|
||||
@ -16,7 +23,7 @@
|
||||
<description>The IP address of the Nuki Bridge. Look it up on your router. It is recommended to set a static IP
|
||||
address lease for the Nuki Bridge (and for your openHAB server too) on your router.</description>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" required="false">
|
||||
<parameter name="port" type="integer" required="true">
|
||||
<label>Port</label>
|
||||
<description>The Port which you configured during Initial Bridge setup
|
||||
(https://nuki.io/en/support/bridge/bridge-setup/initial-bridge-setup/).</description>
|
||||
@ -30,14 +37,31 @@
|
||||
</parameter>
|
||||
<parameter name="manageCallbacks" type="boolean" required="false">
|
||||
<label>Manage Nuki Bridge Callbacks</label>
|
||||
<description>Let the Nuki Binding manage the callback on the Nuki Bridge.</description>
|
||||
<description>
|
||||
Let the Nuki Binding manage the callback on the Nuki Bridge. Nuki bridge uses HTTP callback to notify
|
||||
openHAB about changes in device properties (e.g. when doors are opened, unlocked, doorbell rings, battery level
|
||||
changes etc.). If callback is not registered, binding will not work properly and channels will not be updated. If
|
||||
this is enabled, binding will automatically register and unregister callback as necessary. If this is disabled,
|
||||
user must register callback manually. It is recommended that this is turned on.
|
||||
</description>
|
||||
<default>true</default>
|
||||
</parameter>
|
||||
<parameter name="secureToken" type="boolean" required="false">
|
||||
<label>Secure Token</label>
|
||||
<description>
|
||||
Use hashed token when communicating with bridge. This increases security and prevents sniffing of
|
||||
access token and replay attacks, since communication with bridge is not encrypted. For this feature to work, both
|
||||
device running openHAB and Nuki Bridge must have synchronized time. When disabled, token is sent in plain text with
|
||||
each bridge request. It is recommended that this is turned on unless there are problems with synchronizing time
|
||||
between openHAB and Nuki Bridge.
|
||||
</description>
|
||||
<default>true</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
<!-- Nuki Smart Lock (Thing Type) -->
|
||||
<thing-type id="smartlock">
|
||||
<thing-type id="smartlock" listed="false">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="bridge"/>
|
||||
</supported-bridge-type-refs>
|
||||
@ -47,20 +71,54 @@
|
||||
<channel id="lock" typeId="smartlockLock"/>
|
||||
<channel id="lockState" typeId="smartlockState"/>
|
||||
<channel id="lowBattery" typeId="system.low-battery"/>
|
||||
<channel id="keypadLowBattery" typeId="system.low-battery"/>
|
||||
<channel id="batteryLevel" typeId="system.battery-level"/>
|
||||
<channel id="batteryCharging" typeId="smartLockBatteryCharging"/>
|
||||
<channel id="doorsensorState" typeId="smartlockDoorState"/>
|
||||
</channels>
|
||||
<properties>
|
||||
<property name="name"/>
|
||||
<property name="firmwareVersion"/>
|
||||
</properties>
|
||||
<representation-property>nukiId</representation-property>
|
||||
<config-description>
|
||||
<parameter name="nukiId" type="text" required="true">
|
||||
<label>Nuki ID</label>
|
||||
<description>The 8-digit hexadecimal string that identifies the Nuki Smart Lock. Look it up on the sticker on the
|
||||
back of the Nuki Smart Lock (remove mounting plate).</description>
|
||||
</parameter>
|
||||
<parameter name="unlatch" type="boolean" required="false">
|
||||
<label>Unlatch</label>
|
||||
<description>If switched to On (or set to true) the Nuki Smart Lock will unlock the door but then also automatically
|
||||
pull the latch of the door lock. Usually, if the door hinges are correctly adjusted, the door will then swing open.</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="nukiId" type="text" required="true" readOnly="true">
|
||||
<label>Nuki ID</label>
|
||||
<description>The decimal string that identifies the Nuki Smart Lock.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<!-- Nuki Opener (Thing Type) -->
|
||||
<thing-type id="opener" listed="false">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="bridge"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Nuki Opener</label>
|
||||
<description>Nuki Opener which is paired via Bluetooth to a Nuki Bridge.</description>
|
||||
<channels>
|
||||
<channel id="openerState" typeId="openerState"/>
|
||||
<channel id="openerMode" typeId="openerMode"/>
|
||||
<channel id="openerLowBattery" typeId="system.low-battery"/>
|
||||
<channel id="ringActionState" typeId="ringActionState"/>
|
||||
<channel id="ringActionTimestamp" typeId="ringActionTimestamp"/>
|
||||
</channels>
|
||||
<properties>
|
||||
<property name="name"/>
|
||||
<property name="firmwareVersion"/>
|
||||
</properties>
|
||||
<representation-property>nukiId</representation-property>
|
||||
<config-description>
|
||||
<parameter name="nukiId" type="text" required="true" readOnly="true">
|
||||
<label>Nuki ID</label>
|
||||
<description>The decimal string that identifies the Nuki Opener.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
@ -101,13 +159,37 @@
|
||||
<option value="255">UNDEFINED</option>
|
||||
</options>
|
||||
</state>
|
||||
<command>
|
||||
<options>
|
||||
<option value="1">Unlock</option>
|
||||
<option value="2">Lock</option>
|
||||
<option value="3">Unlatch</option>
|
||||
<option value="4">Lock'n'go</option>
|
||||
<option value="5">Lock'n'go with unlatch</option>
|
||||
</options>
|
||||
</command>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="smartLockBatteryCharging">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Battery Charging</label>
|
||||
<description>Use this channel to display the current state of charging</description>
|
||||
<category>Energy</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="OFF">Battery is not charging</option>
|
||||
<option value="ON">Battery is charging</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="smartlockDoorState">
|
||||
<item-type>Number</item-type>
|
||||
<label>Door State</label>
|
||||
<description>Use this channel to display the current state of the door sensor</description>
|
||||
<category>Door</category>
|
||||
<state>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="0">Unavailable</option>
|
||||
<option value="1">Deactivated</option>
|
||||
@ -117,6 +199,69 @@
|
||||
<option value="5">Calibrating</option>
|
||||
</options>
|
||||
</state>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="openerState">
|
||||
<item-type>Number</item-type>
|
||||
<label>Opener State</label>
|
||||
<description>Use this channel if you want to execute other supported opener actions or to display the current opener
|
||||
state.</description>
|
||||
<category>FrontDoor</category>
|
||||
<state>
|
||||
<options>
|
||||
<option value="0">Untrained</option>
|
||||
<option value="1">Online</option>
|
||||
<option value="3">Ring to open active</option>
|
||||
<option value="5">Open</option>
|
||||
<option value="7">Opening</option>
|
||||
<option value="253">Boot run</option>
|
||||
<option value="255">Undefined</option>
|
||||
</options>
|
||||
</state>
|
||||
<command>
|
||||
<options>
|
||||
<option value="1">Activate ring to open</option>
|
||||
<option value="2">Deactivate ring to open</option>
|
||||
<option value="3">Electric strike actuation</option>
|
||||
<option value="4">Activate continuous mode</option>
|
||||
<option value="5">Deactivate continuous mode</option>
|
||||
</options>
|
||||
</command>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="openerMode">
|
||||
<item-type>Number</item-type>
|
||||
<label>Opener Mode</label>
|
||||
<description>Use this channel to display/set current mode of the opener</description>
|
||||
<category>FrontDoor</category>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="2">Door mode</option>
|
||||
<option value="3">Continuous mode</option>
|
||||
</options>
|
||||
</state>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ringActionState">
|
||||
<kind>trigger</kind>
|
||||
<label>Ring Action State</label>
|
||||
<description>Channel is triggered when doorbell is rang, at most once every 30s</description>
|
||||
<category>Siren</category>
|
||||
<event>
|
||||
<options>
|
||||
<option value="RINGING">Doorbell is ringing</option>
|
||||
</options>
|
||||
</event>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ringActionTimestamp">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Ring Action Timestamp</label>
|
||||
<description>Time of last ring action</description>
|
||||
<category>Siren</category>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
|
Loading…
Reference in New Issue
Block a user