From e835358ce8f413346e20d97f507e7afcc4154ddd Mon Sep 17 00:00:00 2001 From: Bob A Date: Mon, 20 Nov 2023 11:59:44 -0500 Subject: [PATCH 1/4] [sensorpush] Initial commit for SensorPush binding Signed-off-by: Bob Adair --- CODEOWNERS | 1 + bundles/org.openhab.binding.sensorpush/NOTICE | 13 + .../org.openhab.binding.sensorpush/README.md | 89 ++++ .../org.openhab.binding.sensorpush/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../internal/SensorPushBindingConstants.java | 52 +++ .../internal/SensorPushDiscoveryService.java | 106 +++++ .../internal/SensorPushHandlerFactory.java | 71 +++ .../config/CloudBridgeConfiguration.java | 32 ++ .../internal/config/SensorConfiguration.java | 33 ++ .../internal/handler/CloudBridgeHandler.java | 433 ++++++++++++++++++ .../internal/handler/SensorHandler.java | 198 ++++++++ .../internal/protocol/AccessTokenRequest.java | 31 ++ .../protocol/AccessTokenResponse.java | 33 ++ .../protocol/AuthorizationRequest.java | 32 ++ .../protocol/AuthorizationResponse.java | 31 ++ .../internal/protocol/Endpoint.java | 29 ++ .../protocol/InvalidResponseException.java | 35 ++ .../sensorpush/internal/protocol/JwtInfo.java | 76 +++ .../internal/protocol/JwtToken.java | 43 ++ .../internal/protocol/Response.java | 35 ++ .../sensorpush/internal/protocol/Sample.java | 46 ++ .../internal/protocol/SamplesRequest.java | 46 ++ .../internal/protocol/SamplesResponse.java | 50 ++ .../sensorpush/internal/protocol/Sensor.java | 58 +++ .../src/main/resources/OH-INF/addon/addon.xml | 12 + .../resources/OH-INF/thing/thing-types.xml | 174 +++++++ bundles/pom.xml | 1 + 28 files changed, 1786 insertions(+) create mode 100644 bundles/org.openhab.binding.sensorpush/NOTICE create mode 100644 bundles/org.openhab.binding.sensorpush/README.md create mode 100644 bundles/org.openhab.binding.sensorpush/pom.xml create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushBindingConstants.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushDiscoveryService.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushHandlerFactory.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/config/CloudBridgeConfiguration.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/config/SensorConfiguration.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/handler/CloudBridgeHandler.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/handler/SensorHandler.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AccessTokenRequest.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AccessTokenResponse.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AuthorizationRequest.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AuthorizationResponse.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Endpoint.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/InvalidResponseException.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtInfo.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtToken.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Response.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Sample.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/SamplesRequest.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/SamplesResponse.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Sensor.java create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.sensorpush/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index 17363df8e07e..40f435bf4d79 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -305,6 +305,7 @@ /bundles/org.openhab.binding.sensebox/ @hakan42 /bundles/org.openhab.binding.sensibo/ @seime /bundles/org.openhab.binding.sensorcommunity/ @weymann +/bundles/org.openhab.binding.sensorpush/ @bobadair /bundles/org.openhab.binding.serial/ @MikeJMajor /bundles/org.openhab.binding.serialbutton/ @kaikreuzer /bundles/org.openhab.binding.shelly/ @markus7017 diff --git a/bundles/org.openhab.binding.sensorpush/NOTICE b/bundles/org.openhab.binding.sensorpush/NOTICE new file mode 100644 index 000000000000..38d625e34923 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.sensorpush/README.md b/bundles/org.openhab.binding.sensorpush/README.md new file mode 100644 index 000000000000..83daa88501e8 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/README.md @@ -0,0 +1,89 @@ +# SensorPush Binding + +[SensorPush](https://www.sensorpush.com/) sells a line of battery-powered wireless sensors that, depending on the model, provide data on temperature, relative humidity, barometric pressure, dew point, and vapor pressure deficit (VPD). +The sensors communicate using Bluetooth LE. +While they can be used directly via BLE, when multiple sensors are in use they are typically configured to relay data to the cloud via the G1 WiFi Gateway. + +This binding retrieves sensor data from the SensorPush Gateway Cloud using a published API. +It requires use of the G1 WiFi gateway and a connection to the SensorPush Gateway Cloud. + +Supported sensors: HT1, HT.W, and HTP.XW + +## Supported Things + +The binding supports the following thing types: + +* `cloudbridge` - Provides connectivity to the SensorPush Gateway Cloud. +* `sensor` - Represents a HT1, HT.W, or HTP.XW sensor. + +## Discovery + +Automatic discovery is supported for the sensors, but not for the cloud gateway. +It is recommended that you configure the cloudbridge thing manually using the UI and let the associated sensors be discovered automatically. + +## Thing Configuration + +It is recommended that the SensorPush binding be configured through the management UI. +There is no easy way for the user to determine the sensor IDs in advance, so it is best to simply auto-discover them. +After configuring the bridge, all active sensors should appear in the discovery inbox. + +### Cloudbridge thing + +The `cloudbridge` thing is responsible for communicating with the SensorPush Gateway Cloud. +You must supply your user name and password. +The poll and timeout parameters are optional. + +Parameters: + +| Name | Required | Type | Default | Description | +|--------------|----------|---------------|---------|--------------------------| +| `user` | Yes |text | n/a | Your SensorPush cloud service user name (email address) | +| `password`| Yes |text | n/a | Your SensorPush cloud service password | +| `poll` | No |integer, 2-60 | 5 | Polling interval in minutes | +| `timeout` | No |integer, 1-120 | 30 | Timeout period for API requests in seconds. | + +**Note:** To activate your API access, you must log in to the [Gateway Cloud Dashboard](https://dashboard.sensorpush.com/) and agree to the terms of service. +Once you've logged in that initial time, your account will have access. + +Thing config file example: + +``` +Thing sensorpush:cloudbridge:bridge [ user="mickey@disney.com", password="mouse", poll=5 ] +``` + +### Sensor thing + +The `sensor` thing represents an individual SensorPush sensor. +It has a variety of channels that will be populated with the latest sensor readings at each poll period. +Sensors relay their readings at approximate 1 minute intervals, so in normal operation the oldest a reading should be is approximately 1 minute plus the configured poll interval. +The `time` channel will contain the timestamp of the latest readings. +If your particular sensor model does not support a given channel, the value for that channel will remain NULL. +The id parameter must be supplied. + +Parameters: + +| Name | Required | Type | Default | Description | +|-------------------|----------|---------------|---------|------------------------------------| +| `id` | Yes | text | n/a | The unique ID number of the sensor | +| `pressureMode`| No | text | station | The reporting mode for barometric pressure. Must be set to either "station" or "meteorological". The station mode reports the pressure as recorded by the sensor, while the meteorological mode adjusts the reported pressure to mean sea level as is common for weather reporting.| +| `altitude` | No | integer | n/a | The altitude of the sensor in feet above MSL. It is only necessary to set this parameter if you selected the "meteorological" option for pressureMode and have not set the sensor altitude in the SensorPush app. The altitude set in the SensorPush app will override this value.| + +## Channels + +The following channels are provided by the `sensor` thing: + +| Channel | Type | Description | +|-------------|--------------------------|----------------------------------------------------| +| temperature | Number:Temperature | Temperature | +| humidity | Number:Dimensionless | Relative humidity | +| pressure | Number:Pressure | Barometric pressure | +| dewpoint | Number:Temperature | Dew point | +| vpd | Number:Pressure | Vapor pressure deficit | +| time | DateTime | Time of reading | +| rssi | Number | Received signal strength expressed as a number 1-4 | +| rssidbm | Number:Power | Received signal strength in dBm | +| voltage | Number:ElectricPotential | Battery voltage | + +Note that all channels except `time` use QuantityType values. + +No channels are provided by the `cloudbridge` thing. diff --git a/bundles/org.openhab.binding.sensorpush/pom.xml b/bundles/org.openhab.binding.sensorpush/pom.xml new file mode 100644 index 000000000000..6da4e025fcbc --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.1.0-SNAPSHOT + + + org.openhab.binding.sensorpush + + openHAB Add-ons :: Bundles :: SensorPush Binding + + diff --git a/bundles/org.openhab.binding.sensorpush/src/main/feature/feature.xml b/bundles/org.openhab.binding.sensorpush/src/main/feature/feature.xml new file mode 100644 index 000000000000..22511c6e880a --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.sensorpush/${project.version} + + diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushBindingConstants.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushBindingConstants.java new file mode 100644 index 000000000000..92ffa53283df --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushBindingConstants.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link SensorPushBindingConstants} class defines common constants, which are used across the whole binding. + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class SensorPushBindingConstants { + + private static final String BINDING_ID = "sensorpush"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "cloudbridge"); + public static final ThingTypeUID THING_TYPE_SENSOR = new ThingTypeUID(BINDING_ID, "sensor"); + + // Set of discoverable Thing Type UIDs + public static final Set DISCOVERABLE_DEVICE_TYPE_UIDS = Collections.singleton(THING_TYPE_SENSOR); + + // Properties + public static final String PROPERTY_ID = "id"; + public static final String PROPERTY_ADDRESS = "bluetoothAddress"; + + // List of all Channel ids + public static final String CHANNEL_TEMPERATURE = "temperature"; + public static final String CHANNEL_HUMIDITY = "humidity"; + public static final String CHANNEL_TIME = "time"; + public static final String CHANNEL_RSSI = "rssi"; + public static final String CHANNEL_RSSI_DBM = "rssidbm"; + public static final String CHANNEL_VOLTAGE = "voltage"; + public static final String CHANNEL_PRESSURE = "pressure"; + public static final String CHANNEL_DEWPOINT = "dewpoint"; + public static final String CHANNEL_VPD = "vpd"; +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushDiscoveryService.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushDiscoveryService.java new file mode 100644 index 000000000000..602d3e40cef3 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushDiscoveryService.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal; + +import static org.openhab.binding.sensorpush.internal.SensorPushBindingConstants.*; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sensorpush.internal.handler.CloudBridgeHandler; +import org.openhab.binding.sensorpush.internal.protocol.Sensor; +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; + +/** + * The {@link SensorPushDiscoveryService} handles discovery of sensors as they are identified by the bridge handler. + * Requests from the framework to startScan() will initiate a call to the bridge handler's pollSensors() method. + * Otherwise the bridge handler will poll for sensors every other poll interval. + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class SensorPushDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService { + + private final Logger logger = LoggerFactory.getLogger(SensorPushDiscoveryService.class); + + private @NonNullByDefault({}) CloudBridgeHandler bridgeHandler; + private final Set discoveredSensorSet = new HashSet<>(); + + public SensorPushDiscoveryService() { + super(DISCOVERABLE_DEVICE_TYPE_UIDS, 0, false); + } + + @Override + protected void startScan() { + logger.trace("Starting discovery scan"); + discoveredSensorSet.clear(); + if (bridgeHandler != null) { + bridgeHandler.pollSensors(); + } + } + + public void processSensor(Sensor sensor) { + Boolean active = sensor.active; + String deviceId = sensor.deviceId; + String name = sensor.name; + if (deviceId != null && name != null && active != null) { + if (!discoveredSensorSet.contains(deviceId) && active) { + notifyDiscovery(deviceId, name); + discoveredSensorSet.add(deviceId); + } + } else { + logger.debug("Processing sensor with unexpected null values. Ignoring."); + } + } + + private void notifyDiscovery(String idString, String label) { + ThingUID bridgeUID = bridgeHandler.getThing().getUID(); + ThingUID uid = new ThingUID(THING_TYPE_SENSOR, bridgeUID, idString.replace(".", "")); + + Map properties = new HashMap<>(); + properties.put(PROPERTY_ID, idString); + + DiscoveryResult result = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID).withProperties(properties) + .withLabel(label).withRepresentationProperty(PROPERTY_ID).build(); + thingDiscovered(result); + } + + @Override + public void setThingHandler(ThingHandler handler) { + if (handler instanceof CloudBridgeHandler) { + bridgeHandler = (CloudBridgeHandler) handler; + bridgeHandler.setDiscoveryService(this); + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return bridgeHandler; + } + + @Override + public void deactivate() { + super.deactivate(); + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushHandlerFactory.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushHandlerFactory.java new file mode 100644 index 000000000000..4619c370a87e --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/SensorPushHandlerFactory.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal; + +import static org.openhab.binding.sensorpush.internal.SensorPushBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.sensorpush.internal.handler.CloudBridgeHandler; +import org.openhab.binding.sensorpush.internal.handler.SensorHandler; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link SensorPushHandlerFactory} is responsible for creating things and thing handlers. + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.sensorpush", service = ThingHandlerFactory.class) +public class SensorPushHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_SENSOR); + private final HttpClient httpClient; + + @Activate + public SensorPushHandlerFactory(@Reference HttpClientFactory httpClientFactory) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { + return new CloudBridgeHandler((Bridge) thing, httpClient); + } else { + if (THING_TYPE_SENSOR.equals(thingTypeUID)) { + return new SensorHandler(thing); + } + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/config/CloudBridgeConfiguration.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/config/CloudBridgeConfiguration.java new file mode 100644 index 000000000000..c8150389d2f5 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/config/CloudBridgeConfiguration.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CloudBridgeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class CloudBridgeConfiguration { + /** Cloud service user */ + public String user = ""; + /** Cloud service password */ + public String password = ""; + /** Polling interval in minutes */ + public int poll = 5; + /** Request timeout in seconds */ + public int timeout = 30; +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/config/SensorConfiguration.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/config/SensorConfiguration.java new file mode 100644 index 000000000000..d687dd465b30 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/config/SensorConfiguration.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link SensorConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class SensorConfiguration { + /** Sensor ID */ + public String id = ""; + + /** Pressure reporting mode **/ + public String pressureMode = "station"; + + /** Sensor altitude in feet **/ + public @Nullable Integer altitude; +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/handler/CloudBridgeHandler.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/handler/CloudBridgeHandler.java new file mode 100644 index 000000000000..8f1543b57550 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/handler/CloudBridgeHandler.java @@ -0,0 +1,433 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.handler; + +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.openhab.binding.sensorpush.internal.SensorPushDiscoveryService; +import org.openhab.binding.sensorpush.internal.config.CloudBridgeConfiguration; +import org.openhab.binding.sensorpush.internal.protocol.AccessTokenRequest; +import org.openhab.binding.sensorpush.internal.protocol.AccessTokenResponse; +import org.openhab.binding.sensorpush.internal.protocol.AuthorizationRequest; +import org.openhab.binding.sensorpush.internal.protocol.AuthorizationResponse; +import org.openhab.binding.sensorpush.internal.protocol.Endpoint; +import org.openhab.binding.sensorpush.internal.protocol.InvalidResponseException; +import org.openhab.binding.sensorpush.internal.protocol.JwtInfo; +import org.openhab.binding.sensorpush.internal.protocol.Sample; +import org.openhab.binding.sensorpush.internal.protocol.SamplesRequest; +import org.openhab.binding.sensorpush.internal.protocol.SamplesResponse; +import org.openhab.binding.sensorpush.internal.protocol.Sensor; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; + +/** + * The {@link CloudBridgeHandler} is responsible for communicating with the SensorPush cloud. + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class CloudBridgeHandler extends BaseBridgeHandler { + + private static final String AGENT = "openHAB SensorPush Binding"; + private static final String API_URL = "https://api.sensorpush.com"; + private static final int POLL_MIN = 2; + private static final int POLL_MAX = 60; + private static final int TIMEOUT_MIN = 1; + private static final int TIMEOUT_MAX = 120; + + private final Logger logger = LoggerFactory.getLogger(CloudBridgeHandler.class); + + private final HttpClient httpClient; + private final Gson gson; + + private @Nullable SensorPushDiscoveryService discoveryService; + private @Nullable ScheduledFuture pollingJob; + private CloudBridgeConfiguration config = new CloudBridgeConfiguration(); + + private @Nullable String accessToken; + private @Nullable JwtInfo accessTokenInfo; + + private boolean pollSensorsRun = true; + + public CloudBridgeHandler(Bridge bridge, HttpClient httpClient) { + super(bridge); + this.httpClient = httpClient; + gson = new GsonBuilder().create(); + } + + @Override + public Collection> getServices() { + return Collections.singleton(SensorPushDiscoveryService.class); + } + + public void setDiscoveryService(SensorPushDiscoveryService discoveryService) { + this.discoveryService = discoveryService; + } + + @Override + public void initialize() { + config = getConfigAs(CloudBridgeConfiguration.class); + + if (config.user.isBlank() || config.password.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "user/password not configured"); + return; + } + logger.trace("CloudBridge for user {} initializing.", config.user); + + if (config.poll < POLL_MIN) { + config.poll = POLL_MIN; + } else if (config.poll > POLL_MAX) { + config.poll = POLL_MAX; + } + + if (config.timeout < TIMEOUT_MIN) { + config.timeout = TIMEOUT_MIN; + } else if (config.poll > TIMEOUT_MAX) { + config.timeout = TIMEOUT_MAX; + } + + updateStatus(ThingStatus.UNKNOWN); // asyncInitialize will set final status + + scheduler.submit(this::asyncInitialize); + } + + /** + * Perform the init tasks that take a while here because initialize() has to return quickly. + */ + private synchronized void asyncInitialize() { + authorize(); + + if (accessToken != null) { + updateStatus(ThingStatus.ONLINE); + + pollSensors(); + pollSamples(); + } + + ScheduledFuture pollingJob = this.pollingJob; + if (pollingJob == null || pollingJob.isDone()) { + logger.debug("Starting polling job with interval {} minutes", config.poll); + this.pollingJob = scheduler.scheduleWithFixedDelay(this::doPolling, config.poll, config.poll, + TimeUnit.MINUTES); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // No channels defined. Do nothing. + } + + /** + * Post a request to the provided URI with the provided content and optional authToken. + * + * @param uri String containing the URI + * @param content String containing the content + * @param authToken String containing the authorization token to use or null if none + * @return ContentResponse object + * @throws TimeoutException + * @throws InterruptedException + * @throws ExecutionException + */ + private @Nullable ContentResponse sendRequest(String uri, String content, @Nullable String authToken) + throws TimeoutException, InterruptedException, ExecutionException { + ContentResponse response = null; + ContentProvider contentProvider = new StringContentProvider(content); + logger.trace("Using authorization token: {}", authToken); + + Request request = httpClient.POST(uri); + request.agent(AGENT); + request.accept("application/json"); + request.header(HttpHeader.AUTHORIZATION, authToken); + request.timeout(config.timeout, TimeUnit.SECONDS); + request.content(contentProvider, "application/json"); + logger.debug("Sending request: {}", request.toString()); + response = request.send(); + + if (response != null) { + logger.debug("Response received: {} : {}", response.getStatus(), response.getReason()); + logger.trace("Response content: {}", response.getContentAsString()); + } + return response; + } + + /** + * Returns true if the HTTP status is considered good. + */ + private static boolean goodStatus(int status) { + return (status >= 200 && status < 300); + } + + /** + * Perform authorization with the cloud service. Send username/password to get an authorization token, then send the + * authorization token to get an access token. Both are JWT tokens, so we decode the access token to get its + * expiration time. + */ + private void authorize() { + AuthorizationResponse authResponse; + AccessTokenResponse tokenResponse; + + try { + // Send user/password and get authorization token + logger.debug("Getting new authorization token"); + AuthorizationRequest authRequest = new AuthorizationRequest(config.user, config.password); + ContentResponse authContentResponse = sendRequest(API_URL + Endpoint.AUTHORIZE, gson.toJson(authRequest), + null); + + if (authContentResponse == null) { + throw new InvalidResponseException("Auth content response is null"); + } + int status = authContentResponse.getStatus(); + if (status == 403) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid username/password"); + return; + } + if (!goodStatus(authContentResponse.getStatus())) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Auth request returned status " + Integer.valueOf(status)); + return; + } + authResponse = gson.fromJson(authContentResponse.getContentAsString(), AuthorizationResponse.class); + if (authResponse == null) { + throw new InvalidResponseException("Auth response is null"); + } + String authorizationToken = authResponse.authorization; + if (authorizationToken == null) { + throw new InvalidResponseException("Auth token is null"); + } + + // Send authorization token and get access token + logger.debug("Getting new access token"); + String requestContent = gson.toJson(new AccessTokenRequest(authorizationToken)); + ContentResponse accessContentResponse = sendRequest(API_URL + Endpoint.ACCESSTOKEN, requestContent, null); + if (accessContentResponse == null) { + throw new InvalidResponseException("Access token response is null"); + } + if (!goodStatus(accessContentResponse.getStatus())) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Access request returned status " + Integer.valueOf(status)); + return; + } + + tokenResponse = gson.fromJson(accessContentResponse.getContentAsString(), AccessTokenResponse.class); + if (tokenResponse == null) { + throw new InvalidResponseException("Access token object is null"); + } + String accessToken = tokenResponse.accessToken; + logger.trace("Received access token: {}", accessToken); + + JwtInfo jwtInfo; + try { + jwtInfo = new JwtInfo(accessToken); + } catch (IllegalArgumentException e) { + throw new InvalidResponseException("Invalid JWT token"); + } + this.accessToken = accessToken; + accessTokenInfo = jwtInfo; + } catch (TimeoutException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Auth request timeout"); + return; + } catch (InterruptedException e) { + logger.debug("Interrupted sending authorization request"); + return; + } catch (ExecutionException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Auth request execution exception"); + return; + } catch (JsonSyntaxException | InvalidResponseException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error parsing auth response"); + return; + } + } + + /** + * Returns true if the JWT token represented by jwtInfo is expired or will expire in the next 60 seconds. + */ + private boolean tokenExpired(@Nullable JwtInfo jwtInfo) { + if (jwtInfo == null) { + return true; + } else { + return (jwtInfo.expires.minusSeconds(60).isBefore(Instant.now())); + } + } + + /** + * Polling routine. Polls for samples every run, but sensors only every other run. This reduces the polling load on + * the cloud service. + */ + private void doPolling() { + if (accessToken == null || accessTokenInfo == null || tokenExpired(accessTokenInfo)) { + authorize(); + } + + if (pollSensorsRun) { + pollSensors(); + pollSensorsRun = false; + } else { + pollSensorsRun = true; + } + + pollSamples(); + } + + /** + * Poll for sensor info. Also supplies sensor list to discovery. + */ + public void pollSensors() { + logger.trace("Polling sensors"); + + try { + ContentResponse response = sendRequest(API_URL + Endpoint.SENSORS, "{}\r\n", accessToken); + if (response == null) { + throw new InvalidResponseException("Sensor response is null"); + } + if (!goodStatus(response.getStatus())) { + throw new InvalidResponseException( + "Sensors request returned status " + Integer.valueOf(response.getStatus())); + } + + String content = response.getContentAsString(); + Map sensorMap = gson.fromJson(content, new TypeToken>() { + }.getType()); + if (sensorMap == null) { + throw new InvalidResponseException("Sensor map is null"); + } + logger.trace("Received map of {} sensors", sensorMap.size()); + + for (Sensor sensor : sensorMap.values()) { + String deviceId = sensor.deviceId; + logger.trace("Sensor device id: {}", deviceId); + if (deviceId != null) { + notifyChildHandlers(deviceId, null, sensor); + handleDiscovery(sensorMap); + } + } + } catch (TimeoutException | InterruptedException | ExecutionException e) { + logger.debug("Exception requesting sensor data: {}", e.getMessage()); + } catch (InvalidResponseException | JsonParseException e) { + logger.debug("Exception receiving sensor data: {}", e.getMessage()); + } + } + + /** + * Poll for sample data + */ + private void pollSamples() { + logger.trace("Polling samples"); + SamplesRequest requestDTO = new SamplesRequest(); + requestDTO.limit = 1; + requestDTO.measures = SamplesRequest.ALL_MEASUREMENTS; + + try { + ContentResponse response = sendRequest(API_URL + Endpoint.SAMPLES, gson.toJson(requestDTO), accessToken); + if (response == null) { + throw new InvalidResponseException("Sample response is null"); + } + if (!goodStatus(response.getStatus())) { + throw new InvalidResponseException( + "Sample request returned status " + Integer.valueOf(response.getStatus())); + } + + String content = response.getContentAsString(); + SamplesResponse samples = gson.fromJson(content, SamplesResponse.class); + if (samples == null) { + throw new InvalidResponseException("Samples is null"); + } + logger.trace("Received {} samples.", samples.totalSamples); + + Map sensors = samples.sensors; + if (sensors != null) { + for (Map.Entry entry : sensors.entrySet()) { + logger.trace("Sample: id: {} time: {}", entry.getKey(), entry.getValue()[0].observed); + notifyChildHandlers(getDeviceId(entry.getKey()), entry.getValue()[0], null); + } + } + } catch (TimeoutException | InterruptedException | ExecutionException e) { + logger.debug("Exception requesting sample data: {}", e.getMessage()); + } catch (InvalidResponseException | JsonParseException e) { + logger.debug("Exception receiving sample data: {}", e.getMessage()); + } + } + + private void notifyChildHandlers(String deviceId, @Nullable Sample sample, @Nullable Sensor sensor) { + for (Thing thing : getThing().getThings()) { + SensorHandler handler = (SensorHandler) thing.getHandler(); + if (handler != null) { + handler.handleUpdate(deviceId, sample, sensor); + } + } + } + + private void handleDiscovery(Map sensorMap) { + SensorPushDiscoveryService discoveryService = this.discoveryService; + for (Sensor sensor : sensorMap.values()) { + if (discoveryService != null) { + discoveryService.processSensor(sensor); + } + } + } + + /** + * Extracts a short deviceId from a long id string. Returns an empty string if the format of id is invalid. + */ + private String getDeviceId(String id) { + String[] parts = id.split("\\."); + if (parts.length == 2) { + return parts[0]; + } else { + logger.debug("Invalid ID string format: {}", id); + return ""; + } + } + + @Override + public synchronized void dispose() { + logger.trace("Dispose called"); + // Stop polling job + ScheduledFuture pollingJob = this.pollingJob; + if (pollingJob != null) { + pollingJob.cancel(true); + } + accessToken = null; + accessTokenInfo = null; + super.dispose(); + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/handler/SensorHandler.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/handler/SensorHandler.java new file mode 100644 index 000000000000..3447a3b82d39 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/handler/SensorHandler.java @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.handler; + +import static org.openhab.binding.sensorpush.internal.SensorPushBindingConstants.*; +import static org.openhab.core.library.unit.ImperialUnits.*; +import static org.openhab.core.library.unit.MetricPrefix.KILO; +import static org.openhab.core.library.unit.SIUnits.PASCAL; +import static org.openhab.core.library.unit.Units.*; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.sensorpush.internal.config.SensorConfiguration; +import org.openhab.binding.sensorpush.internal.protocol.Sample; +import org.openhab.binding.sensorpush.internal.protocol.Sensor; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SensorHandler} is responsible for handling SensorPush sensor data. + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class SensorHandler extends BaseThingHandler { + + private static final String MODE_STATION = "station"; + private static final String MODE_METEOROLOGICAL = "meteorological"; + + private final Logger logger = LoggerFactory.getLogger(SensorHandler.class); + + private SensorConfiguration config = new SensorConfiguration(); + private @Nullable String address; + private boolean adjustPressure = false; + + /** + * Adjust the supplied atmospheric pressure to mean sea level based on the provided sensor altitude and + * using a standard scale. + * + * @param stationPressure The pressure reading from the sensor in inches or mercury + * @param altitude The altitude of the sensor from MSL in feet + * @return The adjusted atmospheric pressure in inches of mercury (in Hg) + */ + public static Double pressureAdjust(Double stationPressure, int altitude) { + Double hm = 0.3048 * altitude; // convert feet to meters + return stationPressure / Math.pow(((288 - 0.0065 * hm) / 288), 5.2561); + } + + public SensorHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + config = getConfigAs(SensorConfiguration.class); + if (config.id.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "ID not configured"); + return; + } + + logger.debug("Initializing handler for sensor {}", config.id); + + if (MODE_METEOROLOGICAL.equalsIgnoreCase(config.pressureMode)) { + adjustPressure = true; + } else if (!MODE_STATION.equalsIgnoreCase(config.pressureMode)) { + logger.warn("Parameter pressureMode set to invalid value for sensor ID {}. Assuming station mode.", + config.id); + } + + Bridge bridge = getBridge(); + if (bridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured"); + } else { + updateStatus(ThingStatus.UNKNOWN); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // No commands accepted + } + + public void handleUpdate(String id, @Nullable Sample sample, @Nullable Sensor sensor) { + if (config.id.equals(id)) { + logger.trace("Received update for sensor id {}", id); + + if (getThing().getStatusInfo().getStatus() == ThingStatus.UNKNOWN) { + updateStatus(ThingStatus.ONLINE); + } + + if (sample != null) { + Float temperature = sample.temperature; + if (temperature != null) { + updateState(CHANNEL_TEMPERATURE, QuantityType.valueOf(temperature, FAHRENHEIT)); + } + + Float humidity = sample.humidity; + if (humidity != null) { + updateState(CHANNEL_HUMIDITY, QuantityType.valueOf(humidity, PERCENT)); + } + + String observed = sample.observed; + if (observed != null) { + updateState(CHANNEL_TIME, new DateTimeType(observed)); + } + + if (sample.barometricPressure != null) { + Double barometricPressure = sample.barometricPressure.doubleValue(); + if (adjustPressure) { + if (sample.altitude != null) { + barometricPressure = pressureAdjust(barometricPressure, sample.altitude); + } else if (config.altitude != null) { + barometricPressure = pressureAdjust(barometricPressure, config.altitude); + } else { + logger.warn( + "Pressure mode set to Meteorological for sensor {}, but no sensor altitude is configured. Using station pressure.", + config.id); + } + } + updateState(CHANNEL_PRESSURE, QuantityType.valueOf(barometricPressure, INCH_OF_MERCURY)); + } + + Float dewpoint = sample.dewpoint; + if (dewpoint != null) { + updateState(CHANNEL_DEWPOINT, QuantityType.valueOf(dewpoint, FAHRENHEIT)); + } + + Float vpd = sample.vpd; + if (vpd != null) { + updateState(CHANNEL_VPD, QuantityType.valueOf(vpd, KILO(PASCAL))); + } + } + + if (sensor != null) { + Float batteryVoltage = sensor.batteryVoltage; + Integer rssi = sensor.rssi; + if (batteryVoltage != null) { + updateState(CHANNEL_VOLTAGE, QuantityType.valueOf(batteryVoltage, VOLT)); + } + if (rssi != null) { + updateState(CHANNEL_RSSI_DBM, QuantityType.valueOf(rssi, DECIBEL_MILLIWATTS)); + updateState(CHANNEL_RSSI, new DecimalType(rssiBars(rssi).longValue())); + } + + setProperties(sensor); + } + } + } + + /** + * Convert RSSI value in dBm to the 0-4 number required for a channel of type system.signal-strength. An actual + * range of 1-4 is used so that the result matches the signal strength bars displayed in the SensorPush app. + */ + public Long rssiBars(int rssi) { + if (rssi > -50) { + return Long.valueOf(4); + } else if (rssi > -70) { + return Long.valueOf(3); + } else if (rssi > -85) { + return Long.valueOf(2); + } else { + return Long.valueOf(1); + } + } + + public void setProperties(Sensor sensor) { + // Set the bluetooth address property if it isn't set already + String address = sensor.address; + if (this.address == null && address != null) { + Map properties = editProperties(); + properties.put(PROPERTY_ADDRESS, address); + updateProperties(properties); + this.address = address; + } + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AccessTokenRequest.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AccessTokenRequest.java new file mode 100644 index 000000000000..b0a2694c29dd --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AccessTokenRequest.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Access Token Request JSON object + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class AccessTokenRequest extends Response { + + public String authorization; + + /** Construct an AccessTokenRequest using the provided authorization token */ + public AccessTokenRequest(String authToken) { + authorization = authToken; + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AccessTokenResponse.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AccessTokenResponse.java new file mode 100644 index 000000000000..5324183fc44b --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AccessTokenResponse.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +/** + * Access Token Response JSON object + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class AccessTokenResponse extends Response { + + @SerializedName("accesstoken") + public @Nullable String accessToken; + + public AccessTokenResponse() { + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AuthorizationRequest.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AuthorizationRequest.java new file mode 100644 index 000000000000..dae46c77bf2e --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AuthorizationRequest.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Authorization Request JSON object + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class AuthorizationRequest { + + public String email; + public String password; + + public AuthorizationRequest(String email, String password) { + this.email = email; + this.password = password; + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AuthorizationResponse.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AuthorizationResponse.java new file mode 100644 index 000000000000..8e61afe167e7 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/AuthorizationResponse.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Authorization Response JSON object + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class AuthorizationResponse extends Response { + + public @Nullable String authorization; + public @Nullable String apikey; + + public AuthorizationResponse() { + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Endpoint.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Endpoint.java new file mode 100644 index 000000000000..eed85e6210c7 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Endpoint.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * SensorPush REST API endpoint definitions + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class Endpoint { + public static final String AUTHORIZE = "/api/v1/oauth/authorize"; + public static final String ACCESSTOKEN = "/api/v1/oauth/accesstoken"; + public static final String SENSORS = "/api/v1/devices/sensors"; + public static final String GATEWAYS = "/api/v1/devices/gateways"; + public static final String SAMPLES = "/api/v1/samples"; +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/InvalidResponseException.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/InvalidResponseException.java new file mode 100644 index 000000000000..e52104ba9de2 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/InvalidResponseException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Signals than an invalid API response was received + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class InvalidResponseException extends Exception { + + private static final long serialVersionUID = -1025144225509161178L; + + /** + * Constructs a ParseException with the specified detail message. + * + * @param s the detail message + */ + public InvalidResponseException(String s) { + super(s); + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtInfo.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtInfo.java new file mode 100644 index 000000000000..ed5ea87671f3 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtInfo.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import java.nio.charset.Charset; +import java.time.Instant; +import java.util.Base64; +import java.util.Base64.Decoder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +/** + * JSON Web Token (JWT) Info + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class JwtInfo { + + private final Logger logger = LoggerFactory.getLogger(JwtInfo.class); + + public final JwtToken token; + public final Instant expires; + + public JwtInfo(@Nullable String jwtString) { + Gson gson = new Gson(); + Decoder decoder = Base64.getDecoder(); + Charset charset = Charset.forName("UTF-8"); + + JwtToken token; + + if (jwtString == null) { + throw new IllegalArgumentException("Null JWT token String"); + } + String[] sections = jwtString.split("\\."); + if (sections.length != 3) { + logger.debug("JWT token has unexpected number of sections: {}", sections.length); + throw new IllegalArgumentException("Invalid format for JWT token"); + } + byte[] payload = decoder.decode(sections[1]); + String payloadString = new String(payload, charset); + logger.trace("JWT token payload JSON: {}", payloadString); + try { + token = gson.fromJson(payloadString, JwtToken.class); + } catch (JsonSyntaxException e) { + logger.debug("Error parsing JSON in JWT token: {}", e.getMessage()); + throw new IllegalArgumentException("Invalid JSON in JWT token", e); + } + + if (token == null) { + throw new IllegalArgumentException("No content in JWT token"); + } + if (token.exp == null) { + throw new IllegalArgumentException("No exp field in JWT token"); + } + + expires = Instant.ofEpochSecond(token.exp); + this.token = token; + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtToken.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtToken.java new file mode 100644 index 000000000000..55dd05495322 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtToken.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * JWT Token JSON object + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class JwtToken { + + /** Token type */ + public @Nullable String type; + + /** Issuer */ + public @Nullable String iss; + + /** Subject */ + public @Nullable String sub; + + /** Issued at (seconds since epoch) */ + public @Nullable Long iat; + + /** Expires (seconds since epoch) */ + public @NonNullByDefault({}) Long exp; + + public JwtToken() { + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Response.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Response.java new file mode 100644 index 000000000000..99e7857f28ba --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Response.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * API Response JSON object + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public abstract class Response { + + /** May contain message on error */ + public @Nullable String message; + /** May contain error type on error */ + public @Nullable String type; + /** May contain HTTP status code on error */ + public @Nullable String statusCode; + + public Response() { + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Sample.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Sample.java new file mode 100644 index 000000000000..b43bc72c04b9 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Sample.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +/** + * Sensor Sample JSON object + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class Sample { + + /** Configured sensor altitude in feet above MSL **/ + public @Nullable Integer altitude; + /** Temperature in degrees F with 1 digit of precision */ + public @Nullable Float temperature; + /** Relative humidity percentage with 1 digit of precision */ + public @Nullable Float humidity; + /** Barometric pressure in inches of mercury (inHg) */ + @SerializedName("barometric_pressure") + public @Nullable Float barometricPressure; + /** Dew point in degrees F with 1 digit of precision */ + public @Nullable Float dewpoint; + /** Vapor pressure deficit in kPa with 2 digits of precision */ + public @Nullable Float vpd; + /** Timestamp in format 2021-02-25T21:01:37.000Z */ + public @Nullable String observed; + + public Sample() { + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/SamplesRequest.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/SamplesRequest.java new file mode 100644 index 000000000000..43a06e1c3994 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/SamplesRequest.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Samples Request JSON object + * + * @author Bob Adair - Initial contribution + * + * Note: Expected time format is 2019-04-07T00:00:00-0400 + */ +@NonNullByDefault +public class SamplesRequest { + + public static final String[] ALL_MEASUREMENTS = { "temperature", "humidity", "vpd", "barometric_pressure", + "dewpoint", "altitude" }; + + /** Return samples for only active sensors (default=true) */ + public @Nullable Boolean active; + /** Number of samples to return */ + public @Nullable Integer limit; + /** Measurements to return in samples. Valid values: temperature|humidity|vpd|barometric_pressure|dewpoint */ + public @NonNullByDefault({}) String[] measures; + /** Sensor ids to return samples for */ + public @NonNullByDefault({}) String[] sensors; + /** Start time. Leave blank for most recent */ + public @Nullable String startTime; + /** Stop time. Leave blank for most recent */ + public @Nullable String stopTime; + + public SamplesRequest() { + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/SamplesResponse.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/SamplesResponse.java new file mode 100644 index 000000000000..55c9233d346d --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/SamplesResponse.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +/** + * Sample Response JSON object + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class SamplesResponse extends Response { + + @SerializedName("truncated") + public @Nullable Boolean truncated; + + @SerializedName("total_sensors") + public @Nullable Integer totalSensors; + + @SerializedName("last_time") + public @Nullable String lastTime; + + /** Status. "OK" = good */ + public @Nullable String status; + + @SerializedName("total_samples") + public @Nullable Integer totalSamples; + + @SerializedName("sensors") + public @Nullable Map sensors; + + public SamplesResponse() { + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Sensor.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Sensor.java new file mode 100644 index 000000000000..d9c3c0410fd5 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/Sensor.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.sensorpush.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +/** + * Sensor JSON object + * + * @author Bob Adair - Initial contribution + */ +@NonNullByDefault +public class Sensor { + + /** User-supplied sensor name */ + @SerializedName("name") + public @Nullable String name; + + /** Active flag. False for removed sensors. */ + @SerializedName("active") + public @Nullable Boolean active; + + /** Hardware device ID */ + @SerializedName("deviceId") + public @Nullable String deviceId; + + /** MAC Address */ + @SerializedName("address") + public @Nullable String address; + + /** Sensor battery voltage */ + @SerializedName("battery_voltage") + public @Nullable Float batteryVoltage; + + /** RSSI expressed in dBm */ + @SerializedName("rssi") + public @Nullable Integer rssi; + + /** Long form of ID . */ + @SerializedName("id") + public @Nullable String id; + + public Sensor() { + } +} diff --git a/bundles/org.openhab.binding.sensorpush/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.sensorpush/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 000000000000..aea7dfb9a4ce --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,12 @@ + + + + binding + SensorPush Binding + The SensorPush binding interfaces with SensorPush environmental sensors via the G1 WiFi Gateway and the + Gateway cloud service. + cloud + + diff --git a/bundles/org.openhab.binding.sensorpush/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.sensorpush/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 000000000000..69edb3131248 --- /dev/null +++ b/bundles/org.openhab.binding.sensorpush/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,174 @@ + + + + + + SensorPush gateway cloud access + + SensorPush + + + + + + SensorPush gateway cloud username (email address) + + + + password + SensorPush gateway cloud password + + + + The interval at which the handler will poll the cloud service for updates + minutes + 5 + + + + The period in seconds that the handler will wait for a response before timing out. + seconds + 30 + true + + + + + + + + + + + + SensorPush Environmental Sensor + + + + + + + + + + Received signal strength indication (RSSI) + + + + + + + SensorPush + + + id + + + + + Sensor ID + + + + Station mode reports the measured pressure. Meteorological mode adjusts the reported pressure to sea + level. + + + + + true + station + + + + feet + Sensor altitude to use for pressure adjustment. Will be read from the cloud service if available. + + + + + + + Number:Temperature + + Temperature + + Measurement + Temperature + + + + + + Number:Dimensionless + + Humidity + + Measurement + Humidity + + + + + + Number:Pressure + + Vapor pressure deficit + Pressure + + Measurement + Pressure + + + + + + Number:Temperature + + Temperature + + Measurement + Temperature + + + + + + DateTime + + Time of reading + Time + + Measurement + Timestamp + + + + + + Number:Power + + Received signal strength indication (RSSI) in dBm + QualityOfService + + Measurement + Power + + + + + + Number:ElectricPotential + + Battery voltage + Battery + + Measurement + Voltage + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index bc0ef858994b..693b5a4eaa53 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -337,6 +337,7 @@ org.openhab.binding.sensebox org.openhab.binding.sensibo org.openhab.binding.sensorcommunity + org.openhab.binding.sensorpush org.openhab.binding.serial org.openhab.binding.serialbutton org.openhab.binding.shelly From 0011825a4cdb9126bc19547147716825d06e1790 Mon Sep 17 00:00:00 2001 From: Bob A Date: Mon, 20 Nov 2023 13:00:34 -0500 Subject: [PATCH 2/4] [sensorpush] Update addons pom.xml Signed-off-by: Bob Adair --- bom/openhab-addons/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index dadea0ba22ef..e2b49277c43f 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1511,6 +1511,11 @@ org.openhab.binding.sensorcommunity ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.sensorpush + ${project.version} + org.openhab.addons.bundles org.openhab.binding.serial From 6d95972f40f7e0450fb2088ddc3fa6f349593684 Mon Sep 17 00:00:00 2001 From: Bob A Date: Mon, 20 Nov 2023 17:29:40 -0500 Subject: [PATCH 3/4] [sensorpush] Use StandardCharsets Signed-off-by: Bob Adair --- .../openhab/binding/sensorpush/internal/protocol/JwtInfo.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtInfo.java b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtInfo.java index ed5ea87671f3..bea054f7df75 100644 --- a/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtInfo.java +++ b/bundles/org.openhab.binding.sensorpush/src/main/java/org/openhab/binding/sensorpush/internal/protocol/JwtInfo.java @@ -13,6 +13,7 @@ package org.openhab.binding.sensorpush.internal.protocol; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Base64; import java.util.Base64.Decoder; @@ -41,7 +42,7 @@ public class JwtInfo { public JwtInfo(@Nullable String jwtString) { Gson gson = new Gson(); Decoder decoder = Base64.getDecoder(); - Charset charset = Charset.forName("UTF-8"); + Charset charset = StandardCharsets.UTF_8; JwtToken token; From 5164d057ad6b33328d6d688e07c77f846ef2eecc Mon Sep 17 00:00:00 2001 From: Bob A Date: Wed, 22 Nov 2023 18:43:14 -0500 Subject: [PATCH 4/4] [sensorpush] Minor doc update Signed-off-by: Bob Adair --- bundles/org.openhab.binding.sensorpush/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.sensorpush/README.md b/bundles/org.openhab.binding.sensorpush/README.md index 83daa88501e8..3819027544ff 100644 --- a/bundles/org.openhab.binding.sensorpush/README.md +++ b/bundles/org.openhab.binding.sensorpush/README.md @@ -84,6 +84,6 @@ The following channels are provided by the `sensor` thing: | rssidbm | Number:Power | Received signal strength in dBm | | voltage | Number:ElectricPotential | Battery voltage | -Note that all channels except `time` use QuantityType values. +Note that all channels except `time` and `rssi` use QuantityType values. No channels are provided by the `cloudbridge` thing.