Skip to content

Commit

Permalink
[nuki] Opener support and discovery (openhab#10672)
Browse files Browse the repository at this point in the history
* [nuki] Opener support and discovery (openhab#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>
Signed-off-by: Dave J Schoepel <dave@theschoepels.com>
  • Loading branch information
janvyb authored and dschoepel committed Nov 9, 2021
1 parent 2df0ec9 commit 2afce0f
Show file tree
Hide file tree
Showing 32 changed files with 2,230 additions and 733 deletions.
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,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
Expand Down
157 changes: 130 additions & 27 deletions bundles/org.openhab.binding.nuki/README.md
Original file line number Diff line number Diff line change
@@ -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!

Expand All @@ -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:

Expand All @@ -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 |

## Supported Things
### Bridge discovery

This binding support just one thing type: The Nuki Smart Lock. Create one `smartlock` per Nuki Smart Lock available in you home automation environment.
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:

The following configuration options are available:
* 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.

| 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 |
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.

## Supported Channels
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.

- **lock** (Switch)
Use this channel with a Switch Item to lock and unlock the door.
## Supported Things

- **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).
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.

- **lowBattery** (Switch)
Use this channel to receive a low battery warning.
### Nuki Smart Lock

The following configuration options are available:

- **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).
| 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

| 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) |

##### Supported lockState commands

These values can be sent to _lockState_ channel as a commands:

| 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,21 @@
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;
Expand All @@ -39,25 +45,24 @@
* handlers.
*
* @author Markus Katter - Initial contribution
* @contributer Jan Vybíral - Improved thing id generation
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.nuki")
@NonNullByDefault
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
Expand All @@ -67,47 +72,45 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {

@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.
Expand All @@ -116,7 +119,7 @@ public void unregisterHandler(Thing thing) {
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;
}
Expand Down
Loading

0 comments on commit 2afce0f

Please sign in to comment.