mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[flume] Initial contribution (#17152)
* Initial submission Signed-off-by: Jeff James <jeff@james-online.com>
This commit is contained in:
parent
6f6787b794
commit
8f62374b28
@ -566,6 +566,11 @@
|
||||
<artifactId>org.openhab.binding.flicbutton</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.flume</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.fmiweather</artifactId>
|
||||
|
13
bundles/org.openhab.binding.flume/NOTICE
Normal file
13
bundles/org.openhab.binding.flume/NOTICE
Normal file
@ -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
|
120
bundles/org.openhab.binding.flume/README.md
Normal file
120
bundles/org.openhab.binding.flume/README.md
Normal file
@ -0,0 +1,120 @@
|
||||
# Flume Binding
|
||||
|
||||
This binding will interface with the cloud API to retrieve water usage from your [Flume](https://flumewater.com/) water monitor.
|
||||
|
||||
## Introduction
|
||||
|
||||
The Cloud Connector is required as a "bridge" to interface to the cloud service from Flume.
|
||||
While the Flume API supports a rich querying of historical usage data, this binding only retrieves the cumulative water used and instantaneous water used, thus relying on openHAB's rich persistence services for exploring historical values.
|
||||
The binding does support querying historical data through the use of the Rule Action.
|
||||
|
||||
## Supported Things
|
||||
|
||||
This binding supports the following things:
|
||||
|
||||
| Thing | id | Type | Description |
|
||||
|---------- |--------- |-------- |------------------------------ |
|
||||
| Flume Cloud Connector | cloud | Bridge | This represents the cloud account to interface with the Flume API. |
|
||||
| Flume Meter Device | meter-device | Thing | This interfaces to a specific Flume water monitor associated with the account. |
|
||||
|
||||
This binding should work with multiple Flume monitors associated with the account, however it is currently only tested with a single device.
|
||||
|
||||
## Discovery
|
||||
|
||||
Once a Flume Cloud Connector is created and established, the binding will automatically discover any Flume Meter Devices' associated with the account.
|
||||
|
||||
## Flume Cloud Connector (Bridge) Configuration
|
||||
|
||||
The only configuration required is to create a Flume Cloud Connector thing and fill in the appropriate configuration parameters.
|
||||
The client id and client secret can be found under Settings/API access from the [Flume portal online](https://portal.flumewater.com/settings).
|
||||
Note, there is a rate limit of 120 queries per hour imposed by Flume so use caution when selecting the Refresh Interfacl.
|
||||
|
||||
| Name | id | Type | Description | Default | Required | Advanced |
|
||||
|------- |------ |--------- |--------- |------- |------ |----- |
|
||||
| Flume Username | username | text | Username to access Flume cloud | N/A | yes | no |
|
||||
| Flume Password | password | text | Password to access Flume cloud | N/A | yes | no |
|
||||
| Flume Client ID | clientId | text | ID retrieved from Flume cloud | N/A | yes | no |
|
||||
| Flume Client Secret | clientSecret | text | Secret retrieved from Flume cloud | N/A | yes | no |
|
||||
| Instantaneous Refresh Interval | refreshIntervalInstantaneous | integer | Polling interval (minutes) for instantaneous usage (rate limited to 120 queries/sec) | 1 | no | yes |
|
||||
| Cumulative Refresh Interval | refreshIntervalCumulative | integer | Polling interval (minutes) for cumulative usage (rate-limited with above) | 5 | no | yes |
|
||||
|
||||
## Flume Meter Device Configuration
|
||||
|
||||
| Name | id | Type | Description | Default | Required | Advanced |
|
||||
|------- |--------- |------ |--------- |------- |------ |----- |
|
||||
| ID | id | text | ID of the Flume device | N/A | yes | no |
|
||||
|
||||
## Flume Meter Device Channels
|
||||
|
||||
| Channel | id | Type | Read/Write | Description |
|
||||
|---------- |-------- |-------- |-------- |-------- |
|
||||
| Instant Water Usage | instant-usage | Number:VolumetricFlowRate | R | Flow rate of water over the last minute |
|
||||
| Cumulative Used | cumulative-usage | Number:Volume | R | Total volume of water used since the beginning of Flume install |
|
||||
| Battery Level | battery-level | Number:Dimensionless | R | Estimate of percent of remaining battery level |
|
||||
| Low Battery | low-battery | Switch | R | Indicator of low battery level |
|
||||
| Last Seen | last-seen | DateTime | R | Date/Time when meter was last seen on the network |
|
||||
| Usage Alert | usage-alert | Trigger | n/a | Trigger channel for usage alert notification |
|
||||
|
||||
## Full Example
|
||||
|
||||
### Thing Configuration
|
||||
|
||||
Please note that the device meter ID is only available through the API and not available on the Flume portal.
|
||||
When the Bridge device is first created, there will be a log message with the ID of the discovered device which can be used in further configuring the device via the text files.
|
||||
|
||||
```
|
||||
Bridge flume:cloud:cloudconnector [ username="xxx", password="xxx", clientId="xxx", clientSecret="xxx" ] {
|
||||
|
||||
meter-device meter [ id="xxx" ]
|
||||
}
|
||||
```
|
||||
|
||||
### Item Configuration
|
||||
|
||||
```
|
||||
Number:VolumetricFlowRate InstantUsage "Instant Usage" { channel = "flume:meter-device:1:meter:instant-usage" }
|
||||
Number:Volume CumulativeUsed "Cumulative Used" { channel = "flume:meter-device:1:meter:cumulative-usage" }
|
||||
Number:Dimensionless BatteryLevel "Battery Level" { channel = "flume:meter-device:1:meter:battery-level" }
|
||||
DateTime LastSeen "Last Seen" { channel = "flume:meter-device:1:meter:last-seen" }
|
||||
Switch LowPower "Battery Low Power" { channel = "flume:meter-device:1:meter:low-battery" }
|
||||
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
```java
|
||||
rule "Flume Usage Alert"
|
||||
when
|
||||
Channel 'flume:device:cloud:meter:usageAlert' triggered
|
||||
then
|
||||
logInfo("Flume Usage Alert", "Message: {}", receivedEvent)
|
||||
end
|
||||
```
|
||||
|
||||
## Rule Actions
|
||||
|
||||
There is an action where you can query the Flume Cloud for water usage as shown in the blow example:
|
||||
|
||||
```java
|
||||
val flumeActions = getActions("flume", "flume:device:cloud:meter")
|
||||
|
||||
if(null === flumeActions) {
|
||||
logInfo("actions", "flumeActions not found, check thing ID")
|
||||
return
|
||||
}
|
||||
|
||||
val LocalDateTime untilDateTime = LocalDateTime.now
|
||||
val LocalDateTime sinceDateTime = untilDateTime.minusHours(24)
|
||||
|
||||
val usage = flumeActions.queryWaterUsage(sinceDateTime, untilDateTime, "MIN", "SUM")
|
||||
logInfo("Flume", "Water usage is {}", usage.toString())
|
||||
```
|
||||
|
||||
### queryWaterUsage(sinceDateTime, untilDateTime, bucket, operation)
|
||||
|
||||
Queries the cloud for water usage between the two dates.
|
||||
|
||||
- sinceDateTime (LocalDateTime): begin date/time of query range
|
||||
- untilDateTime (LocalDateTime): end date/time of query range
|
||||
- bucket (String), values: YR, MON, DAY, HR, MIN
|
||||
- operation (String), values: SUM, AVG, MIN, MAX, CNT
|
17
bundles/org.openhab.binding.flume/pom.xml
Normal file
17
bundles/org.openhab.binding.flume/pom.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.flume</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Flume Binding</name>
|
||||
|
||||
</project>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.flume-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-flume" description="Flume Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.flume/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link FlumeBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FlumeBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "flume";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_CLOUD = new ThingTypeUID(BINDING_ID, "cloud");
|
||||
public static final ThingTypeUID THING_TYPE_METER = new ThingTypeUID(BINDING_ID, "meter-device");
|
||||
|
||||
// Config options
|
||||
public static final String PARAM_USERNAME = "username";
|
||||
public static final String PARAM_PASSWORD = "password";
|
||||
public static final String PARAM_CLIENTID = "clientId";
|
||||
public static final String PARAM_CLIENTSECRET = "clientSecret";
|
||||
public static final String PARAM_REFRESH_INTERVAL_INSTANTANEOUS = "refreshIntervalInstanteous";
|
||||
public static final String PARAM_REFRESH_INTERVAL_CUMULATIVE = "refreshIntervalCumulative";
|
||||
|
||||
// List of all Device Channel ids
|
||||
public static final String CHANNEL_DEVICE_CUMULATIVEUSAGE = "cumulative-usage";
|
||||
public static final String CHANNEL_DEVICE_INSTANTUSAGE = "instant-usage";
|
||||
public static final String CHANNEL_DEVICE_BATTERYLEVEL = "battery-level";
|
||||
public static final String CHANNEL_DEVICE_LOWBATTERY = "low-battery";
|
||||
public static final String CHANNEL_DEVICE_LASTSEEN = "last-seen";
|
||||
public static final String CHANNEL_DEVICE_USAGEALERT = "usage-alert";
|
||||
|
||||
// Properties
|
||||
public static final String PROPERTY_ID = "id";
|
||||
|
||||
public static final int DEFAULT_POLLING_INTERVAL_INSTANTANEOUS = 1;
|
||||
public static final int DEFAULT_POLLING_INTERVAL_CUMULATIVE = 5;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal;
|
||||
|
||||
import static org.openhab.binding.flume.internal.FlumeBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link FlumeBridgeConfig} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FlumeBridgeConfig {
|
||||
public String clientId = "";
|
||||
public String clientSecret = "";
|
||||
public String username = "";
|
||||
public String password = "";
|
||||
|
||||
public int refreshIntervalInstantaneous = DEFAULT_POLLING_INTERVAL_INSTANTANEOUS;
|
||||
public int refreshIntervalCumulative = DEFAULT_POLLING_INTERVAL_CUMULATIVE;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link FlumeDeviceConfig} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FlumeDeviceConfig {
|
||||
public String id = "";
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal;
|
||||
|
||||
import static org.openhab.binding.flume.internal.FlumeBindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import javax.measure.spi.SystemOfUnits;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.flume.internal.handler.FlumeBridgeHandler;
|
||||
import org.openhab.binding.flume.internal.handler.FlumeDeviceHandler;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
import org.openhab.core.i18n.UnitProvider;
|
||||
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 FlumeHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.flume", service = ThingHandlerFactory.class)
|
||||
public class FlumeHandlerFactory extends BaseThingHandlerFactory {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_CLOUD, THING_TYPE_METER);
|
||||
|
||||
private final HttpClientFactory httpClientFactory;
|
||||
private final TranslationProvider i18nProvider;
|
||||
private final LocaleProvider localeProvider;
|
||||
public final SystemOfUnits systemOfUnits;
|
||||
|
||||
@Activate
|
||||
public FlumeHandlerFactory(@Reference UnitProvider unitProvider, @Reference HttpClientFactory httpClientFactory,
|
||||
final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider) {
|
||||
this.systemOfUnits = unitProvider.getMeasurementSystem();
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
this.i18nProvider = i18nProvider;
|
||||
this.localeProvider = localeProvider;
|
||||
}
|
||||
|
||||
@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_CLOUD.equals(thingTypeUID)) {
|
||||
return new FlumeBridgeHandler((Bridge) thing, systemOfUnits, this.httpClientFactory.getCommonHttpClient(),
|
||||
i18nProvider, localeProvider);
|
||||
} else if (THING_TYPE_METER.equals(thingTypeUID)) {
|
||||
return new FlumeDeviceHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.actions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.measure.quantity.Volume;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.flume.internal.api.FlumeApi;
|
||||
import org.openhab.binding.flume.internal.api.FlumeApiException;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiQueryWaterUsage;
|
||||
import org.openhab.binding.flume.internal.handler.FlumeDeviceHandler;
|
||||
import org.openhab.core.automation.annotation.ActionInput;
|
||||
import org.openhab.core.automation.annotation.ActionOutput;
|
||||
import org.openhab.core.automation.annotation.RuleAction;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.ImperialUnits;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.binding.ThingActions;
|
||||
import org.openhab.core.thing.binding.ThingActionsScope;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ServiceScope;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link FlumeDeviceActions} class defines actions for the Flume Device
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@Component(scope = ServiceScope.PROTOTYPE, service = FlumeDeviceActions.class)
|
||||
@ThingActionsScope(name = "flume")
|
||||
@NonNullByDefault
|
||||
public class FlumeDeviceActions implements ThingActions {
|
||||
private final Logger logger = LoggerFactory.getLogger(FlumeDeviceActions.class);
|
||||
private static final String QUERYID = "action_query";
|
||||
|
||||
private @Nullable FlumeDeviceHandler deviceHandler;
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
if (handler instanceof FlumeDeviceHandler deviceHandler) {
|
||||
this.deviceHandler = deviceHandler;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return deviceHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query water usage
|
||||
*/
|
||||
@RuleAction(label = "query water usage", description = "Queries water usage over a period of time.")
|
||||
public @Nullable @ActionOutput(name = "value", type = "QuantityType<Volume>") QuantityType<Volume> queryWaterUsage(
|
||||
@ActionInput(name = "sinceDateTime", label = "Since Date/Time", required = true, description = "Restrict the query range to data samples since this datetime.") @Nullable LocalDateTime sinceDateTime,
|
||||
@ActionInput(name = "untilDateTime", label = "Until Date/Time", required = true, description = "Restrict the query range to data samples until this datetime.") @Nullable LocalDateTime untilDateTime,
|
||||
@ActionInput(name = "bucket", label = "Bucket size", required = true, description = "The bucket grouping of the data we are querying (MIN, HR, DAY, MON, YR).") @Nullable String bucket,
|
||||
@ActionInput(name = "operation", label = "Operation", required = true, description = "The aggregate/accumulate operation to perform (SUM, AVG, MIN, MAX, CNT).") @Nullable String operation) {
|
||||
logger.info("queryWaterUsage called");
|
||||
|
||||
FlumeApiQueryWaterUsage query = new FlumeApiQueryWaterUsage();
|
||||
|
||||
FlumeDeviceHandler localDeviceHandler = deviceHandler;
|
||||
if (localDeviceHandler == null) {
|
||||
logger.debug("querying device usage, but device is undefined.");
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean imperialUnits = localDeviceHandler.isImperial();
|
||||
|
||||
if (operation == null || bucket == null || sinceDateTime == null || untilDateTime == null) {
|
||||
logger.warn("queryWaterUsage called with null inputs");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!FlumeApi.OperationType.contains(operation)) {
|
||||
logger.warn("Invalid aggregation operation in call to queryWaterUsage");
|
||||
return null;
|
||||
} else {
|
||||
query.operation = FlumeApi.OperationType.valueOf(operation);
|
||||
}
|
||||
|
||||
if (!FlumeApi.BucketType.contains(bucket)) {
|
||||
logger.warn("Invalid bucket type in call to queryWaterUsage");
|
||||
return null;
|
||||
} else {
|
||||
query.bucket = FlumeApi.BucketType.valueOf(bucket);
|
||||
}
|
||||
|
||||
if (untilDateTime.isBefore(sinceDateTime)) {
|
||||
logger.warn("sinceDateTime must be earlier than untilDateTime");
|
||||
return null;
|
||||
}
|
||||
|
||||
query.requestId = QUERYID;
|
||||
query.sinceDateTime = sinceDateTime;
|
||||
query.untilDateTime = untilDateTime;
|
||||
query.bucket = FlumeApi.BucketType.valueOf(bucket);
|
||||
query.units = imperialUnits ? FlumeApi.UnitType.GALLONS : FlumeApi.UnitType.LITERS;
|
||||
|
||||
Float usage;
|
||||
try {
|
||||
usage = localDeviceHandler.getApi().queryUsage(localDeviceHandler.getId(), query);
|
||||
} catch (FlumeApiException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
|
||||
logger.warn("queryWaterUsage function failed - {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
if (usage == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new QuantityType<Volume>(usage, imperialUnits ? ImperialUnits.GALLON_LIQUID_US : Units.LITRE);
|
||||
}
|
||||
|
||||
// Static method for Rules DSL backward compatibility
|
||||
public static @Nullable QuantityType<Volume> queryWaterUsage(ThingActions actions,
|
||||
@Nullable LocalDateTime sinceDateTime, @Nullable LocalDateTime untilDateTime, @Nullable String bucket,
|
||||
@Nullable String operation) {
|
||||
if (actions instanceof FlumeDeviceActions localActions) {
|
||||
return localActions.queryWaterUsage(sinceDateTime, untilDateTime, bucket, operation);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not a FlumeDeviceActions class.");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,439 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiCurrentFlowRate;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiDevice;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiGetToken;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiQueryBucket;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiQueryWaterUsage;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiRefreshToken;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiToken;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiTokenPayload;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiUsageAlert;
|
||||
import org.openhab.binding.flume.utils.JsonInstantSerializer;
|
||||
import org.openhab.binding.flume.utils.JsonLocalDateTimeSerializer;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* The {@link FlumeApi} implements the interface to the Flume cloud service (using http). The documentation for the API
|
||||
* is located here: https://flumetech.readme.io/reference
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FlumeApi {
|
||||
private final Logger logger = LoggerFactory.getLogger(FlumeApi.class);
|
||||
|
||||
// --------------- Flume Cloud API
|
||||
public static final String APIURL_BASE = "https://api.flumewater.com/";
|
||||
|
||||
public static final String APIURL_TOKEN = "oauth/token";
|
||||
public static final String APIURL_GETUSERSDEVICES = "users/%s/devices?user=%s&location=%s";
|
||||
public static final String APIURL_GETDEVICEINFO = "users/%s/devices/%s";
|
||||
public static final String APIURL_QUERYUSAGE = "users/%s/devices/%s/query";
|
||||
public static final String APIURL_FETCHUSAGEALERTS = "users/%s/usage-alerts?device_id=%s&limit=%d&sort_field=%s&sort_direction=%s";
|
||||
public static final String APIURL_FETCHNOTIFICATIONS = "users/%s/notifications?device_id=%s&limit=%d&sort_field=%s&sort_direction=%s";
|
||||
public static final String APIURL_GETCURRENTFLOWRATE = "users/%s/devices/%s/query/active";
|
||||
|
||||
private static final int API_TIMEOUT = 15;
|
||||
|
||||
// @formatter:off
|
||||
public enum UnitType {
|
||||
GALLONS, LITERS, CUBIC_FEET, CUBIC_METERS
|
||||
}
|
||||
|
||||
public enum OperationType {
|
||||
SUM, AVG, MIN, MAX, CNT;
|
||||
|
||||
public static boolean contains(String value) {
|
||||
return Arrays.stream(values()).anyMatch((t) -> t.name().equals(value));
|
||||
}
|
||||
}
|
||||
|
||||
public enum BucketType {
|
||||
YR, MON, DAY, HR, MIN;
|
||||
|
||||
public static boolean contains(String value) {
|
||||
return Arrays.stream(values()).anyMatch((t) -> t.name().equals(value));
|
||||
}
|
||||
}
|
||||
|
||||
public enum SortDirectionType {
|
||||
ASC, DESC
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
protected String clientId = "";
|
||||
protected String clientSecret = "";
|
||||
protected String username = "";
|
||||
protected String password = "";
|
||||
protected Gson gson;
|
||||
|
||||
private String accessToken = "";
|
||||
private String refreshToken = "";
|
||||
private int userId;
|
||||
private LocalDateTime tokenExpiresAt = LocalDateTime.now();
|
||||
|
||||
private HttpClient httpClient;
|
||||
|
||||
public FlumeApi(HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
this.gson = new GsonBuilder()
|
||||
.registerTypeAdapter(LocalDateTime.class, new JsonLocalDateTimeSerializer("yyyy-MM-dd HH:mm:ss")) // 2022-07-13
|
||||
// 20:14:00
|
||||
.registerTypeAdapter(Instant.class, new JsonInstantSerializer()) // 2022-07-14T03:13:00.000Z
|
||||
.create();
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public void initialize(String clientId, String clientSecret, String username, String password, ThingUID bridgeUID)
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
|
||||
getToken();
|
||||
}
|
||||
|
||||
private void getToken() throws FlumeApiException, IOException, InterruptedException, TimeoutException,
|
||||
ExecutionException, NullPointerException {
|
||||
FlumeApiGetToken getToken = new FlumeApiGetToken();
|
||||
|
||||
getToken.clientId = clientId;
|
||||
getToken.clientSecret = clientSecret;
|
||||
getToken.username = username;
|
||||
getToken.password = password;
|
||||
|
||||
String url = APIURL_BASE + APIURL_TOKEN;
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.POST)
|
||||
.content(new StringContentProvider(gson.toJson(getToken)), MediaType.APPLICATION_JSON);
|
||||
|
||||
JsonObject jsonResponse = sendAndValidate(request, false);
|
||||
|
||||
final FlumeApiToken[] data = gson.fromJson(jsonResponse.get("data").getAsJsonArray(), FlumeApiToken[].class);
|
||||
|
||||
if (data == null) {
|
||||
throw new FlumeApiException("@text/api.response-invalid", jsonResponse.get("code").getAsInt(), true);
|
||||
}
|
||||
|
||||
processToken(data[0]);
|
||||
}
|
||||
|
||||
private void refreshToken()
|
||||
throws IOException, InterruptedException, TimeoutException, ExecutionException, FlumeApiException {
|
||||
FlumeApiRefreshToken token = new FlumeApiRefreshToken();
|
||||
|
||||
token.clientId = clientId;
|
||||
token.clientSecret = clientSecret;
|
||||
token.refeshToken = refreshToken;
|
||||
|
||||
String url = APIURL_BASE + APIURL_TOKEN;
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.POST)
|
||||
.content(new StringContentProvider(gson.toJson(token)), MediaType.APPLICATION_JSON);
|
||||
|
||||
JsonObject jsonResponse = sendAndValidate(request, false);
|
||||
|
||||
final FlumeApiToken[] data = gson.fromJson(jsonResponse.get("data").getAsJsonArray(), FlumeApiToken[].class);
|
||||
|
||||
if (data == null || data.length < 1) {
|
||||
throw new FlumeApiException("@text/api.response-invalid", jsonResponse.get("code").getAsInt(), true);
|
||||
}
|
||||
|
||||
processToken(data[0]);
|
||||
}
|
||||
|
||||
private void processToken(FlumeApiToken token) throws FlumeApiException {
|
||||
accessToken = token.accessToken;
|
||||
|
||||
// access_token contains 3 parts: header, payload, signature - decode the payload portion
|
||||
String accessTokenPayload[] = accessToken.split("\\.");
|
||||
byte decoded[] = Base64.getDecoder().decode(accessTokenPayload[1]);
|
||||
|
||||
String jsonPayload = new String(decoded);
|
||||
|
||||
final FlumeApiTokenPayload payload = gson.fromJson(jsonPayload, FlumeApiTokenPayload.class);
|
||||
|
||||
if (payload == null) {
|
||||
throw new FlumeApiException("@text/api.response-invalid", 0, true);
|
||||
}
|
||||
|
||||
userId = payload.userId;
|
||||
|
||||
refreshToken = token.refreshToken;
|
||||
tokenExpiresAt = LocalDateTime.now().plusSeconds(token.expiresIn * 2 / 3);
|
||||
|
||||
logger.debug("Token expires at: {}", tokenExpiresAt);
|
||||
}
|
||||
|
||||
public void verifyToken()
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
if (LocalDateTime.now().isAfter(tokenExpiresAt)) {
|
||||
refreshToken();
|
||||
}
|
||||
}
|
||||
|
||||
public List<FlumeApiDevice> getDeviceList()
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
String url = APIURL_BASE + String.format(APIURL_GETUSERSDEVICES, this.userId, false, false);
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
JsonObject jsonResponse = sendAndValidate(request);
|
||||
|
||||
final FlumeApiDevice[] listDevices = gson.fromJson(jsonResponse.get("data").getAsJsonArray(),
|
||||
FlumeApiDevice[].class);
|
||||
|
||||
return Arrays.asList(listDevices);
|
||||
}
|
||||
|
||||
/**
|
||||
* gets Flume device info
|
||||
*
|
||||
* @param deviceId for the device
|
||||
* @return FlumeApiDevice dto structure
|
||||
*
|
||||
* @throws FlumeApiException
|
||||
* @throws IOException
|
||||
* @throws InterruptedException
|
||||
* @throws TimeoutException
|
||||
* @throws ExecutionException
|
||||
*/
|
||||
public @Nullable FlumeApiDevice getDeviceInfo(String deviceId)
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
String url = APIURL_BASE + String.format(APIURL_GETDEVICEINFO, this.userId, deviceId);
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
JsonObject jsonResponse = sendAndValidate(request);
|
||||
|
||||
final FlumeApiDevice[] apiDevices = gson.fromJson(jsonResponse.get("data").getAsJsonArray(),
|
||||
FlumeApiDevice[].class);
|
||||
|
||||
return (apiDevices == null || apiDevices.length == 0) ? null : apiDevices[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* makes a single query to the API.
|
||||
*
|
||||
* @param deviceID for the device
|
||||
* @param query FlumeApiQueryWaterUsage class with query parameters
|
||||
* @return the result of the single query
|
||||
*
|
||||
* @throws FlumeApiException
|
||||
* @throws IOException
|
||||
* @throws InterruptedException
|
||||
* @throws TimeoutException
|
||||
* @throws ExecutionException
|
||||
*/
|
||||
public @Nullable Float queryUsage(String deviceID, FlumeApiQueryWaterUsage query)
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
List<FlumeApiQueryWaterUsage> listQuery = new ArrayList<FlumeApiQueryWaterUsage>();
|
||||
List<HashMap<String, List<FlumeApiQueryBucket>>> queryData;
|
||||
|
||||
listQuery.add(query);
|
||||
|
||||
queryData = queryUsage(deviceID, listQuery);
|
||||
if (queryData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, List<FlumeApiQueryBucket>> queryBuckets = queryData.get(0);
|
||||
|
||||
List<FlumeApiQueryBucket> queryBucket = queryBuckets.get(query.requestId);
|
||||
|
||||
return (queryBucket == null || queryBucket.isEmpty()) ? null : queryBucket.get(0).value;
|
||||
}
|
||||
|
||||
/**
|
||||
* makes multiple queries to the API combined into a single Rest API request.
|
||||
*
|
||||
* @param deviceID for the device
|
||||
* @param listQuery a List of FlumeApiQueryWaterUsage query parameters
|
||||
* @return a list of HashMap
|
||||
*
|
||||
* @throws FlumeApiException
|
||||
* @throws IOException
|
||||
* @throws InterruptedException
|
||||
* @throws TimeoutException
|
||||
* @throws ExecutionException
|
||||
*/
|
||||
public @Nullable List<HashMap<String, List<FlumeApiQueryBucket>>> queryUsage(String deviceID,
|
||||
List<FlumeApiQueryWaterUsage> listQuery)
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
if (listQuery.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String jsonQuery = "{\"queries\":" + gson.toJson(listQuery) + "}";
|
||||
|
||||
String url = APIURL_BASE + String.format(APIURL_QUERYUSAGE, this.userId, deviceID);
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.POST)
|
||||
.content(new StringContentProvider(jsonQuery), MediaType.APPLICATION_JSON);
|
||||
|
||||
logger.debug("METADATA: {}", jsonQuery);
|
||||
JsonObject jsonResponse = sendAndValidate(request);
|
||||
|
||||
final Type queryResultType = new TypeToken<List<HashMap<String, List<FlumeApiQueryBucket>>>>() {
|
||||
}.getType();
|
||||
|
||||
List<HashMap<String, List<FlumeApiQueryBucket>>> listQueryResult = gson.fromJson(jsonResponse.get("data"),
|
||||
queryResultType);
|
||||
|
||||
return listQueryResult;
|
||||
}
|
||||
|
||||
public @Nullable FlumeApiCurrentFlowRate getCurrentFlowRate(String deviceId)
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
String url = APIURL_BASE + String.format(APIURL_GETCURRENTFLOWRATE, this.userId, deviceId);
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
JsonObject jsonResponse = sendAndValidate(request);
|
||||
|
||||
final FlumeApiCurrentFlowRate[] currentFlowRates = gson.fromJson(jsonResponse.get("data").getAsJsonArray(),
|
||||
FlumeApiCurrentFlowRate[].class);
|
||||
|
||||
return (currentFlowRates == null || currentFlowRates.length < 1) ? null : currentFlowRates[0];
|
||||
}
|
||||
|
||||
public List<FlumeApiUsageAlert> fetchUsageAlerts(String deviceId, int limit)
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
String url = APIURL_BASE
|
||||
+ String.format(APIURL_FETCHUSAGEALERTS, userId, deviceId, limit, "triggered_datetime", "DESC");
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
JsonObject jsonResponse = sendAndValidate(request);
|
||||
|
||||
final FlumeApiUsageAlert[] listUsageAlerts = gson.fromJson(jsonResponse.get("data").getAsJsonArray(),
|
||||
FlumeApiUsageAlert[].class);
|
||||
|
||||
return Arrays.asList(listUsageAlerts);
|
||||
}
|
||||
|
||||
public List<FlumeApiUsageAlert> fetchNotificatinos(String deviceId, int limit)
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
String url = APIURL_BASE
|
||||
+ String.format(APIURL_FETCHNOTIFICATIONS, userId, deviceId, limit, "triggered_datetime", "DEC");
|
||||
Request request = httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
|
||||
JsonObject jsonResponse = sendAndValidate(request);
|
||||
|
||||
final FlumeApiUsageAlert[] listUsageAlerts = gson.fromJson(jsonResponse.get("data").getAsJsonArray(),
|
||||
FlumeApiUsageAlert[].class);
|
||||
|
||||
return Arrays.asList(listUsageAlerts);
|
||||
}
|
||||
|
||||
private JsonObject sendAndValidate(Request request)
|
||||
throws FlumeApiException, InterruptedException, TimeoutException, ExecutionException, IOException {
|
||||
return sendAndValidate(request, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* does routine setup, validation and conversion to JsonObject for http requests
|
||||
*
|
||||
* @param request to be sent
|
||||
* @param verifyToken whether the exisitng access token should be validate and refreshed if needed
|
||||
* @return JsonObject from the Rest API call
|
||||
*
|
||||
* @throws FlumeApiException
|
||||
* @throws InterruptedException
|
||||
* @throws TimeoutException
|
||||
* @throws ExecutionException
|
||||
* @throws IOException
|
||||
*/
|
||||
private JsonObject sendAndValidate(Request request, boolean verifyToken)
|
||||
throws FlumeApiException, InterruptedException, TimeoutException, ExecutionException, IOException {
|
||||
ContentResponse response;
|
||||
|
||||
if (verifyToken) {
|
||||
verifyToken();
|
||||
}
|
||||
|
||||
setHeaders(request);
|
||||
|
||||
logger.debug("REQUEST: {}", request.toString());
|
||||
response = request.send();
|
||||
logger.trace("RESPONSE: {}", response.getContentAsString());
|
||||
|
||||
switch (response.getStatus()) {
|
||||
case 200:
|
||||
break;
|
||||
case 400:
|
||||
// Flume API sense response code 400 (vs. normal 401) on invalid user credentials
|
||||
throw new FlumeApiException("@text/api.invalid-user-credentials [\"" + response.getReason() + "\"]",
|
||||
response.getStatus(), true);
|
||||
case 401:
|
||||
throw new FlumeApiException("@text/api.invalid-user-credentials [\"" + response.getReason() + "\"]",
|
||||
response.getStatus(), true);
|
||||
case 429:
|
||||
logger.trace("rate limit response: {}", response.getContentAsString());
|
||||
throw new FlumeApiException("@text/api.rate-limit-exceeded", 429, false);
|
||||
default:
|
||||
throw new FlumeApiException("", response.getStatus(), false);
|
||||
}
|
||||
|
||||
JsonObject jsonResponse = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
|
||||
boolean success = jsonResponse.get("success").getAsBoolean();
|
||||
|
||||
if (!success) {
|
||||
String message = jsonResponse.get("message").getAsString();
|
||||
throw new FlumeApiException("@text/api.query-fail [\"" + message + "\"]",
|
||||
jsonResponse.get("code").getAsInt(), false);
|
||||
}
|
||||
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
private Request setHeaders(Request request) {
|
||||
if (!accessToken.isEmpty()) {
|
||||
request.header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken);
|
||||
}
|
||||
request.timeout(API_TIMEOUT, TimeUnit.SECONDS);
|
||||
return request;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.api;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* {@link FlumeApiException} exception class for any api exception
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FlumeApiException extends Exception {
|
||||
private static final long serialVersionUID = -7050804598914012847L;
|
||||
private int code;
|
||||
private boolean configurationIssue;
|
||||
|
||||
public FlumeApiException(String message, int code, boolean configurationIssue) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.configurationIssue = configurationIssue;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public boolean isConfigurationIssue() {
|
||||
return configurationIssue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getMessage() {
|
||||
return super.getMessage();
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.api.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* The {@link FlumeApiCurrentFlowRate} dto for getCurrentFlowRate
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class FlumeApiCurrentFlowRate {
|
||||
public boolean active;
|
||||
public float gpm;
|
||||
public LocalDateTime datetime;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.api.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link FlumeApiDevice} dto for FetchUsersDevices.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class FlumeApiDevice {
|
||||
public String id = ""; // "id": "6248148189204194987",
|
||||
@SerializedName("bridge_id")
|
||||
public String bridgeId; // "bridge_id": "6248148189204155555",
|
||||
public int type; // Bridge devices have type=1. Sensor devices have type=2
|
||||
public String name;
|
||||
public String description;
|
||||
@SerializedName("added_datetime")
|
||||
public String addedDateTime; // "added_datetime": "2017-03-16T14:30:13.284Z",
|
||||
@SerializedName("last_seen")
|
||||
public Instant lastSeen; // "last_seen": "2017-04-13T01:31:36.000Z",
|
||||
@SerializedName("battery_level")
|
||||
public String batteryLevel;
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link FlumeApiGetToken} dto for Get Token post.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class FlumeApiGetToken {
|
||||
@SerializedName("grant_type")
|
||||
public final String grantType = "password";
|
||||
@SerializedName("client_id")
|
||||
public String clientId;
|
||||
@SerializedName("client_secret")
|
||||
public String clientSecret;
|
||||
public String username;
|
||||
public String password;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.api.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* The {@link FlumeApiQueryBucket} dto for query water usage.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class FlumeApiQueryBucket {
|
||||
public LocalDateTime datetime; // "datetime": "2016-03-01 00:30:00"
|
||||
public float value; // "value": 2.7943592
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.api.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.openhab.binding.flume.internal.api.FlumeApi;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link FlumeApiQueryWaterUsage} dto for setting up query of water usage.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class FlumeApiQueryWaterUsage {
|
||||
@SerializedName("request_id")
|
||||
public String requestId;
|
||||
@SerializedName("since_datetime")
|
||||
public LocalDateTime sinceDateTime;
|
||||
@SerializedName("until_datetime")
|
||||
public LocalDateTime untilDateTime;
|
||||
@SerializedName("tz")
|
||||
public String timeZone;
|
||||
public FlumeApi.BucketType bucket;
|
||||
@SerializedName("device_id")
|
||||
public String[] deviceId;
|
||||
@SerializedName("group_multiplier")
|
||||
public Integer groupMultiplier;
|
||||
public FlumeApi.OperationType operation;
|
||||
public FlumeApi.UnitType units;
|
||||
@SerializedName("sort_direction")
|
||||
public FlumeApi.SortDirectionType sortDirection;
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link FlumeApiRefreshToken} dto for refresh token
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class FlumeApiRefreshToken {
|
||||
@SerializedName("grant_type")
|
||||
public final String grantType = "refresh_token";
|
||||
@SerializedName("client_id")
|
||||
public String clientId;
|
||||
@SerializedName("client_secret")
|
||||
public String clientSecret;
|
||||
@SerializedName("refresh_token")
|
||||
public String refeshToken;
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link FlumeApiToken} dto response for getToken.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class FlumeApiToken {
|
||||
@SerializedName("token_type")
|
||||
public String tokenType;
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link FlumeApiTokenPayload} dto for Token payload.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class FlumeApiTokenPayload {
|
||||
@SerializedName("user_id")
|
||||
public int userId;
|
||||
public String type;
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.api.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link FlumeApiUsageAlert} dto for querying usage alerts.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
public class FlumeApiUsageAlert {
|
||||
public int id;
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
@SerializedName("triggered_datetime")
|
||||
public Instant triggeredDateTime;
|
||||
@SerializedName("flume_leak")
|
||||
public boolean leak;
|
||||
public FlumeApiQueryWaterUsage query;
|
||||
@SerializedName("event_rule_name")
|
||||
public String eventRuleName;
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* {@link FlumeCloudConnectorConfig} implements the http-based REST API to access the Flume Cloud
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FlumeCloudConnectorConfig {
|
||||
public String username = "";
|
||||
public String password = "";
|
||||
public String clientId = "";
|
||||
public String clientSecret = "";
|
||||
public int pollingInterval;
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.flume.internal.FlumeBindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.flume.internal.handler.FlumeBridgeHandler;
|
||||
import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ServiceScope;
|
||||
|
||||
/**
|
||||
* The {@link FlumeDiscoveryService} implements discovers service for bridge
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@Component(scope = ServiceScope.PROTOTYPE, service = FlumeDiscoveryService.class, configurationPid = "discovery.flume")
|
||||
@NonNullByDefault
|
||||
public class FlumeDiscoveryService extends AbstractThingHandlerDiscoveryService<FlumeBridgeHandler>
|
||||
implements ThingHandlerService {
|
||||
private static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Set.of(THING_TYPE_METER);
|
||||
|
||||
public FlumeDiscoveryService() {
|
||||
super(FlumeBridgeHandler.class, DISCOVERABLE_THING_TYPES_UIDS, 0, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
thingHandler.registerDiscoveryListener(this);
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
thingHandler.unregisterDiscoveryListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void startScan() {
|
||||
thingHandler.refreshDevices(true);
|
||||
}
|
||||
|
||||
public void notifyDiscoveryDevice(String id) {
|
||||
ThingUID bridgeUID = thingHandler.getThing().getUID();
|
||||
|
||||
ThingUID uid = new ThingUID(THING_TYPE_METER, bridgeUID, id);
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID).withProperty(PROPERTY_ID, id)
|
||||
.withRepresentationProperty(PROPERTY_ID).withLabel("Flume Meter Device").build();
|
||||
thingDiscovered(result);
|
||||
}
|
||||
}
|
@ -0,0 +1,298 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.handler;
|
||||
|
||||
import static org.openhab.binding.flume.internal.FlumeBindingConstants.THING_TYPE_METER;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.measure.spi.SystemOfUnits;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.flume.internal.FlumeBridgeConfig;
|
||||
import org.openhab.binding.flume.internal.api.FlumeApi;
|
||||
import org.openhab.binding.flume.internal.api.FlumeApiException;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiDevice;
|
||||
import org.openhab.binding.flume.internal.discovery.FlumeDiscoveryService;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.osgi.framework.Bundle;
|
||||
import org.osgi.framework.FrameworkUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link FlumeBridgeHandler} implements the Flume bridge cloud connector
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FlumeBridgeHandler extends BaseBridgeHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(FlumeBridgeHandler.class);
|
||||
|
||||
public FlumeBridgeConfig config = new FlumeBridgeConfig();
|
||||
|
||||
private static final Duration CACHE_EXPIRY = Duration.ofMinutes(30);
|
||||
private ExpiringCache<List<FlumeApiDevice>> apiListDevicesCache = new ExpiringCache<>(CACHE_EXPIRY,
|
||||
this::apiListDevicesAction);
|
||||
|
||||
private boolean logOnce = false;
|
||||
|
||||
private final FlumeApi api;
|
||||
final SystemOfUnits systemOfUnits;
|
||||
final TranslationProvider i18nProvider;
|
||||
final LocaleProvider localeProvider;
|
||||
final Bundle bundle;
|
||||
|
||||
public FlumeApi getApi() {
|
||||
return api;
|
||||
}
|
||||
|
||||
protected @Nullable ScheduledFuture<?> pollingJob;
|
||||
private @Nullable FlumeDiscoveryService discoveryService;
|
||||
|
||||
/**
|
||||
* Get the services registered for this bridge. Provides the discovery service.
|
||||
*/
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Set.of(FlumeDiscoveryService.class);
|
||||
}
|
||||
|
||||
public boolean registerDiscoveryListener(FlumeDiscoveryService listener) {
|
||||
if (discoveryService == null) {
|
||||
discoveryService = listener;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean unregisterDiscoveryListener() {
|
||||
if (discoveryService != null) {
|
||||
discoveryService = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public FlumeBridgeHandler(final Bridge bridge, SystemOfUnits systemOfUnits, HttpClient httpClient,
|
||||
TranslationProvider i18nProvider, LocaleProvider localeProvider) {
|
||||
super(bridge);
|
||||
|
||||
api = new FlumeApi(httpClient);
|
||||
this.systemOfUnits = systemOfUnits;
|
||||
this.i18nProvider = i18nProvider;
|
||||
this.localeProvider = localeProvider;
|
||||
this.bundle = FrameworkUtil.getBundle(this.getClass());
|
||||
}
|
||||
|
||||
public FlumeBridgeConfig getFlumeBridgeConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(FlumeBridgeConfig.class);
|
||||
|
||||
if (config.clientId.isBlank() | config.clientSecret.isBlank() || config.password.isBlank()
|
||||
|| config.username.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
|
||||
"@text/offline.cloud-configuration-error");
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
scheduler.execute(this::goOnline);
|
||||
}
|
||||
|
||||
public void goOnline() {
|
||||
try {
|
||||
api.initialize(config.clientId, config.clientSecret, config.username, config.password,
|
||||
this.getThing().getUID());
|
||||
} catch (FlumeApiException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
|
||||
handleApiException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!refreshDevices(true)) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.cloud-configuration-error");
|
||||
return;
|
||||
}
|
||||
|
||||
int pollingPeriod = Math.min(config.refreshIntervalCumulative, config.refreshIntervalInstantaneous);
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(this::pollDevices, 0, pollingPeriod, TimeUnit.MINUTES);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<FlumeApiDevice> apiListDevicesAction() {
|
||||
try {
|
||||
return api.getDeviceList();
|
||||
} catch (FlumeApiException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
|
||||
handleApiException(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update the listDevicesCache if expired or forcedUpdate. Will iterate through the list and
|
||||
* either notify of discovery to discoveryService or, if the device is already configured, will update
|
||||
* the device info.
|
||||
*
|
||||
* @param forcedUpdate force update
|
||||
* @return true if successful in querying the API
|
||||
*/
|
||||
public boolean refreshDevices(boolean forcedUpdate) {
|
||||
final FlumeDiscoveryService discovery = discoveryService;
|
||||
|
||||
if (forcedUpdate) {
|
||||
apiListDevicesCache.invalidateValue();
|
||||
}
|
||||
@Nullable
|
||||
List<FlumeApiDevice> listDevices = apiListDevicesCache.getValue();
|
||||
|
||||
if (listDevices == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (FlumeApiDevice dev : listDevices) {
|
||||
if (dev.type == 2 && discovery != null) {
|
||||
FlumeDeviceHandler deviceHandler = getFlumeDeviceHandler(dev.id);
|
||||
|
||||
if (deviceHandler == null) {
|
||||
// output ID of discovered device to log once to identify ID so it can be used for textual
|
||||
// configuration
|
||||
if (!logOnce) {
|
||||
logger.info("Flume Meter Device Discovered: ID: {}", dev.id);
|
||||
logOnce = true;
|
||||
}
|
||||
discovery.notifyDiscoveryDevice(dev.id);
|
||||
} else {
|
||||
deviceHandler.updateDeviceInfo(dev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* iterates through the child things to find the handler with the matching id
|
||||
*
|
||||
* @param id of the Flume device thing to find
|
||||
* @return FlumeDeviceHandler or null
|
||||
*/
|
||||
@Nullable
|
||||
public FlumeDeviceHandler getFlumeDeviceHandler(String id) {
|
||||
//@formatter:off
|
||||
return getThing().getThings().stream()
|
||||
.filter(t -> t.getThingTypeUID().equals(THING_TYPE_METER))
|
||||
.map(t -> (FlumeDeviceHandler)t.getHandler())
|
||||
.filter(Objects::nonNull)
|
||||
.filter(h -> h.getId().equals(id))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
//@formatter:on
|
||||
}
|
||||
|
||||
public void handleApiException(Exception e) {
|
||||
if (e instanceof FlumeApiException flumeApiException) {
|
||||
if (flumeApiException.isConfigurationIssue()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
|
||||
flumeApiException.getLocalizedMessage());
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
|
||||
flumeApiException.getLocalizedMessage());
|
||||
}
|
||||
} else if (e instanceof IOException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||
} else if (e instanceof InterruptedIOException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||
} else if (e instanceof InterruptedException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||
} else if (e instanceof TimeoutException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||
} else if (e instanceof ExecutionException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||
} else {
|
||||
// capture in log since this is an unexpected exception
|
||||
logger.warn("Unhandled Exception", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final ChannelUID channelUID, final Command command) {
|
||||
// cloud handler has no channels
|
||||
}
|
||||
|
||||
/**
|
||||
* iterates through all child things to update usage
|
||||
*/
|
||||
private void pollDevices() {
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
// try to go online if it is offline due to communication error
|
||||
if (getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.COMMUNICATION_ERROR) {
|
||||
goOnline();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// refresh listDevicesCache if necessary
|
||||
if (apiListDevicesCache.isExpired()) {
|
||||
refreshDevices(true);
|
||||
}
|
||||
|
||||
//@formatter:off
|
||||
getThing().getThings().stream()
|
||||
.forEach(t -> { if(t.getHandler() instanceof FlumeDeviceHandler handler) { handler.queryUsage(); } });
|
||||
//@formatter:on
|
||||
}
|
||||
|
||||
public @Nullable String getLocaleString(String key) {
|
||||
return i18nProvider.getText(bundle, key, null, localeProvider.getLocale());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void dispose() {
|
||||
ScheduledFuture<?> localPollingJob = pollingJob;
|
||||
if (localPollingJob != null) {
|
||||
localPollingJob.cancel(true);
|
||||
pollingJob = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,494 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.internal.handler;
|
||||
|
||||
import static org.openhab.binding.flume.internal.FlumeBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.flume.internal.FlumeBridgeConfig;
|
||||
import org.openhab.binding.flume.internal.FlumeDeviceConfig;
|
||||
import org.openhab.binding.flume.internal.actions.FlumeDeviceActions;
|
||||
import org.openhab.binding.flume.internal.api.FlumeApi;
|
||||
import org.openhab.binding.flume.internal.api.FlumeApiException;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiCurrentFlowRate;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiDevice;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiQueryBucket;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiQueryWaterUsage;
|
||||
import org.openhab.binding.flume.internal.api.dto.FlumeApiUsageAlert;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.ImperialUnits;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
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.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.BridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link FlumeDeviceHandler} is the implementation the flume meter device.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FlumeDeviceHandler extends BaseThingHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(FlumeDeviceHandler.class);
|
||||
|
||||
// private final static String beginDateUsage = "2016-01-01 00:00:00";
|
||||
private static final LocalDateTime BEGIN_DATE_USAGE = LocalDateTime.of(2016, 1, 1, 0, 0, 0);
|
||||
private static final String QUERY_ID_CUMULATIVE_START_OF_YEAR = "cumulativeStartOfYear";
|
||||
private static final String QUERY_ID_YEAR_TO_DATE = "usageYTD";
|
||||
|
||||
private ExpiringCache<FlumeApiDevice> apiDeviceCache = new ExpiringCache<FlumeApiDevice>(
|
||||
Duration.ofMinutes(5).toMillis(), this::getDeviceInfo);
|
||||
|
||||
private FlumeDeviceConfig config = new FlumeDeviceConfig();
|
||||
|
||||
private float cumulativeStartOfYear = 0;
|
||||
|
||||
private float cumulativeUsage = 0;
|
||||
private long expiryCumulativeUsage = 0;
|
||||
private Duration refreshIntervalCumulative = Duration.ofMinutes(DEFAULT_POLLING_INTERVAL_CUMULATIVE);
|
||||
|
||||
private float instantUsage = 0;
|
||||
private long expiryInstantUsage = 0;
|
||||
private Duration refreshIntervalInstant = Duration.ofMinutes(DEFAULT_POLLING_INTERVAL_INSTANTANEOUS);
|
||||
|
||||
private LocalDateTime startOfYear = LocalDateTime.MIN;
|
||||
|
||||
private Instant lastUsageAlert = Instant.MIN;
|
||||
|
||||
private static final Duration USAGE_QUERY_FETCH_INTERVAL = Duration.ofMinutes(5);
|
||||
private long expiryUsageAlertFetch = 0;
|
||||
|
||||
public FlumeDeviceHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(FlumeDeviceConfig.class);
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
scheduler.execute(this::goOnline);
|
||||
}
|
||||
|
||||
public void goOnline() {
|
||||
if (this.getThing().getStatus() == ThingStatus.ONLINE) {
|
||||
return;
|
||||
}
|
||||
|
||||
FlumeBridgeHandler bh = getBridgeHandler();
|
||||
|
||||
if (bh == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.configuration-error.bridge-missing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bh.getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
return;
|
||||
}
|
||||
|
||||
FlumeBridgeConfig bridgeConfig = bh.getFlumeBridgeConfig();
|
||||
|
||||
refreshIntervalCumulative = Duration.ofMinutes(bridgeConfig.refreshIntervalCumulative);
|
||||
refreshIntervalInstant = Duration.ofMinutes(bridgeConfig.refreshIntervalInstantaneous);
|
||||
|
||||
// always update the startOfYear number;
|
||||
startOfYear = LocalDateTime.MIN;
|
||||
|
||||
FlumeApiDevice apiDevice = apiDeviceCache.getValue();
|
||||
if (apiDevice != null) {
|
||||
updateDeviceInfo(apiDevice);
|
||||
}
|
||||
|
||||
try {
|
||||
tryQueryUsage(true);
|
||||
tryGetCurrentFlowRate(true);
|
||||
} catch (FlumeApiException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
|
||||
handleApiException(e);
|
||||
}
|
||||
|
||||
lastUsageAlert = Instant.now(); // don't retrieve any usage alerts prior to going online
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
} else if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
|
||||
goOnline();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services registered for this bridge. Provides the discovery service.
|
||||
*/
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Set.of(FlumeDeviceActions.class);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return config.id;
|
||||
}
|
||||
|
||||
public void updateDeviceChannel(@Nullable FlumeApiDevice apiDevice, String channelUID) {
|
||||
final Map<String, Integer> mapBatteryLevel = Map.of("low", 25, "medium", 50, "high", 100);
|
||||
if (apiDevice == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Integer percent = mapBatteryLevel.get(apiDevice.batteryLevel);
|
||||
if (percent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelUID) {
|
||||
case CHANNEL_DEVICE_BATTERYLEVEL:
|
||||
updateState(CHANNEL_DEVICE_BATTERYLEVEL, new QuantityType<>(percent, Units.PERCENT));
|
||||
break;
|
||||
case CHANNEL_DEVICE_LOWBATTERY:
|
||||
updateState(CHANNEL_DEVICE_LOWBATTERY, (percent <= 25) ? OnOffType.ON : OnOffType.OFF);
|
||||
break;
|
||||
case CHANNEL_DEVICE_LASTSEEN:
|
||||
updateState(CHANNEL_DEVICE_LASTSEEN,
|
||||
new DateTimeType(ZonedDateTime.ofInstant(apiDevice.lastSeen, ZoneId.systemDefault())));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void handleApiException(Exception e) {
|
||||
if (e instanceof FlumeApiException flumeApiException) {
|
||||
if (flumeApiException.isConfigurationIssue()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
|
||||
flumeApiException.getLocalizedMessage());
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
|
||||
flumeApiException.getLocalizedMessage());
|
||||
}
|
||||
} else if (e instanceof IOException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getLocalizedMessage());
|
||||
} else if (e instanceof InterruptedIOException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||
} else if (e instanceof InterruptedException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||
} else if (e instanceof TimeoutException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||
} else if (e instanceof ExecutionException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||
} else {
|
||||
// capture in log since this is an unexpected exception
|
||||
logger.warn("Unhandled Exception", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pools together several usage queries based on whether the value is expired and whether a channel is linked. Also,
|
||||
* if necessary will update the usage from beginning to start of year so subsequent cumulative queries only need to
|
||||
* ytd. Will update the values in the ExpiringCache as necessary.
|
||||
*
|
||||
* @throws FlumeApiException
|
||||
* @throws IOException
|
||||
* @throws InterruptedException
|
||||
* @throws TimeoutException
|
||||
* @throws ExecutionException
|
||||
*/
|
||||
protected void tryQueryUsage(boolean forceUpdate)
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
@Nullable
|
||||
List<HashMap<String, List<FlumeApiQueryBucket>>> result;
|
||||
|
||||
boolean imperialUnits = isImperial();
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
List<FlumeApiQueryWaterUsage> listQuery = new ArrayList<FlumeApiQueryWaterUsage>();
|
||||
|
||||
// Get sum of all historical readings only when binding starts or its the start of a new year
|
||||
// This is to reduce the time it takes on the periodic queries
|
||||
if (startOfYear.equals(LocalDateTime.MIN) || (now.getYear() != startOfYear.getYear())) {
|
||||
FlumeApiQueryWaterUsage query = new FlumeApiQueryWaterUsage();
|
||||
|
||||
query.bucket = FlumeApi.BucketType.YR;
|
||||
query.sinceDateTime = BEGIN_DATE_USAGE;
|
||||
query.untilDateTime = now.minusYears(1);
|
||||
query.groupMultiplier = 100;
|
||||
query.operation = FlumeApi.OperationType.SUM;
|
||||
query.requestId = QUERY_ID_CUMULATIVE_START_OF_YEAR;
|
||||
query.units = imperialUnits ? FlumeApi.UnitType.GALLONS : FlumeApi.UnitType.LITERS;
|
||||
|
||||
listQuery.add(query);
|
||||
}
|
||||
|
||||
if (System.nanoTime() > this.expiryUsageAlertFetch) {
|
||||
fetchUsageAlerts();
|
||||
this.expiryUsageAlertFetch = System.nanoTime() + USAGE_QUERY_FETCH_INTERVAL.toNanos();
|
||||
}
|
||||
|
||||
if (this.isLinked(CHANNEL_DEVICE_CUMULATIVEUSAGE)
|
||||
&& ((System.nanoTime() > this.expiryCumulativeUsage) || forceUpdate)) {
|
||||
FlumeApiQueryWaterUsage query = new FlumeApiQueryWaterUsage();
|
||||
|
||||
query.bucket = FlumeApi.BucketType.DAY;
|
||||
query.untilDateTime = now;
|
||||
query.sinceDateTime = now.withDayOfYear(1);
|
||||
query.groupMultiplier = 400;
|
||||
query.operation = FlumeApi.OperationType.SUM;
|
||||
query.requestId = QUERY_ID_YEAR_TO_DATE;
|
||||
query.units = imperialUnits ? FlumeApi.UnitType.GALLONS : FlumeApi.UnitType.LITERS;
|
||||
|
||||
listQuery.add(query);
|
||||
}
|
||||
|
||||
if (listQuery.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
result = getApi().queryUsage(config.id, listQuery);
|
||||
|
||||
if (result == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/offline.cloud-connection-issue");
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, List<FlumeApiQueryBucket>> queryData = result.get(0);
|
||||
List<FlumeApiQueryBucket> queryBuckets;
|
||||
|
||||
queryBuckets = queryData.get(QUERY_ID_CUMULATIVE_START_OF_YEAR);
|
||||
if (queryBuckets != null) {
|
||||
cumulativeStartOfYear = queryBuckets.get(0).value;
|
||||
startOfYear = now.withDayOfYear(1);
|
||||
}
|
||||
|
||||
queryBuckets = queryData.get(QUERY_ID_YEAR_TO_DATE);
|
||||
if (queryBuckets != null) {
|
||||
cumulativeUsage = queryBuckets.get(0).value + cumulativeStartOfYear;
|
||||
updateState(CHANNEL_DEVICE_CUMULATIVEUSAGE,
|
||||
new QuantityType<>(cumulativeUsage, imperialUnits ? ImperialUnits.GALLON_LIQUID_US : Units.LITRE));
|
||||
this.expiryCumulativeUsage = System.nanoTime() + this.refreshIntervalCumulative.toNanos();
|
||||
}
|
||||
}
|
||||
|
||||
protected void tryGetCurrentFlowRate(boolean forceUpdate)
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
if (this.isLinked(CHANNEL_DEVICE_INSTANTUSAGE)
|
||||
&& ((System.nanoTime() > this.expiryInstantUsage) || forceUpdate)) {
|
||||
FlumeApiCurrentFlowRate currentFlowRate = getApi().getCurrentFlowRate(config.id);
|
||||
if (currentFlowRate == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
instantUsage = currentFlowRate.gpm;
|
||||
updateState(CHANNEL_DEVICE_INSTANTUSAGE, new QuantityType<>(instantUsage, ImperialUnits.GALLON_PER_MINUTE));
|
||||
this.expiryInstantUsage = System.nanoTime() + this.refreshIntervalInstant.toNanos();
|
||||
}
|
||||
}
|
||||
|
||||
protected @Nullable FlumeApiDevice tryGetDeviceInfo()
|
||||
throws FlumeApiException, IOException, InterruptedException, TimeoutException, ExecutionException {
|
||||
FlumeApiDevice deviceInfo = getApi().getDeviceInfo(config.id);
|
||||
if (deviceInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
protected @Nullable FlumeApiDevice getDeviceInfo() {
|
||||
try {
|
||||
return this.tryGetDeviceInfo();
|
||||
} catch (FlumeApiException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
|
||||
handleApiException(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void queryUsage() {
|
||||
// Try to go online if the device was previously taken offline due to connection issues w/ cloud
|
||||
if (getThing().getStatus() == ThingStatus.OFFLINE
|
||||
&& getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.COMMUNICATION_ERROR) {
|
||||
goOnline();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
tryQueryUsage(false);
|
||||
tryGetCurrentFlowRate(false);
|
||||
} catch (FlumeApiException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
|
||||
this.handleApiException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (System.nanoTime() > this.expiryUsageAlertFetch) {
|
||||
fetchUsageAlerts();
|
||||
this.expiryUsageAlertFetch = System.nanoTime() + USAGE_QUERY_FETCH_INTERVAL.toNanos();
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchUsageAlerts() {
|
||||
List<FlumeApiUsageAlert> resultList;
|
||||
FlumeApiUsageAlert alert;
|
||||
FlumeApiQueryWaterUsage query;
|
||||
|
||||
boolean imperialUnits = isImperial();
|
||||
|
||||
try {
|
||||
resultList = getApi().fetchUsageAlerts(config.id, 1);
|
||||
} catch (FlumeApiException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
|
||||
this.handleApiException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
alert = resultList.get(0);
|
||||
// alert has already been notified or occurred before the device went online
|
||||
if (!alert.triggeredDateTime.isAfter(this.lastUsageAlert)) {
|
||||
logger.trace("alert: {}, lastUsageAlert: {}", alert.triggeredDateTime, this.lastUsageAlert);
|
||||
return;
|
||||
}
|
||||
|
||||
lastUsageAlert = alert.triggeredDateTime;
|
||||
|
||||
String stringAlertFormat = Objects.requireNonNull(getBridgeHandler())
|
||||
.getLocaleString("trigger.high-flow-alert");
|
||||
if (stringAlertFormat == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
query = alert.query;
|
||||
query.bucket = FlumeApi.BucketType.MIN;
|
||||
query.operation = FlumeApi.OperationType.AVG;
|
||||
query.units = imperialUnits ? FlumeApi.UnitType.GALLONS : FlumeApi.UnitType.LITERS;
|
||||
|
||||
Float avgUsage;
|
||||
try {
|
||||
avgUsage = getApi().queryUsage(config.id, query);
|
||||
} catch (FlumeApiException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
|
||||
this.handleApiException(e);
|
||||
return;
|
||||
}
|
||||
long minutes = Duration.between(query.sinceDateTime, query.untilDateTime).toMinutes();
|
||||
|
||||
LocalDateTime localWhenTriggered = LocalDateTime.ofInstant(alert.triggeredDateTime, ZoneId.systemDefault());
|
||||
|
||||
String stringAlert = String.format(stringAlertFormat, alert.eventRuleName, localWhenTriggered.toString(),
|
||||
minutes, avgUsage, imperialUnits ? "gallons" : "liters");
|
||||
|
||||
triggerChannel(CHANNEL_DEVICE_USAGEALERT, stringAlert);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_DEVICE_CUMULATIVEUSAGE:
|
||||
try {
|
||||
tryQueryUsage(true);
|
||||
} catch (FlumeApiException | IOException | InterruptedException | TimeoutException
|
||||
| ExecutionException e) {
|
||||
handleApiException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
case CHANNEL_DEVICE_INSTANTUSAGE:
|
||||
try {
|
||||
tryGetCurrentFlowRate(true);
|
||||
} catch (FlumeApiException | IOException | InterruptedException | TimeoutException
|
||||
| ExecutionException e) {
|
||||
handleApiException(e);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_DEVICE_BATTERYLEVEL:
|
||||
updateDeviceChannel(apiDeviceCache.getValue(), CHANNEL_DEVICE_BATTERYLEVEL);
|
||||
break;
|
||||
case CHANNEL_DEVICE_LOWBATTERY:
|
||||
updateDeviceChannel(apiDeviceCache.getValue(), CHANNEL_DEVICE_LOWBATTERY);
|
||||
break;
|
||||
case CHANNEL_DEVICE_LASTSEEN:
|
||||
updateDeviceChannel(apiDeviceCache.getValue(), CHANNEL_DEVICE_LASTSEEN);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateDeviceInfo(FlumeApiDevice apiDevice) {
|
||||
apiDeviceCache.putValue(apiDevice);
|
||||
|
||||
updateDeviceChannel(apiDevice, CHANNEL_DEVICE_BATTERYLEVEL);
|
||||
updateDeviceChannel(apiDevice, CHANNEL_DEVICE_LOWBATTERY);
|
||||
updateDeviceChannel(apiDevice, CHANNEL_DEVICE_LASTSEEN);
|
||||
}
|
||||
|
||||
public boolean isImperial() {
|
||||
return Objects.requireNonNull(getBridgeHandler()).systemOfUnits instanceof ImperialUnits;
|
||||
}
|
||||
|
||||
public @Nullable FlumeBridgeHandler getBridgeHandler() {
|
||||
Bridge bridge = this.getBridge();
|
||||
if (bridge == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bridge.getHandler() instanceof FlumeBridgeHandler bridgeHandler) {
|
||||
return bridgeHandler;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public FlumeApi getApi() {
|
||||
Bridge bridge = Objects.requireNonNull(getBridge());
|
||||
BridgeHandler handler = Objects.requireNonNull(bridge.getHandler());
|
||||
|
||||
return ((FlumeBridgeHandler) handler).getApi();
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.utils;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
/**
|
||||
* {@link JsonInstantSerializer} implements gson serializer for Java Instant
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class JsonInstantSerializer implements JsonSerializer<Instant>, JsonDeserializer<Instant> {
|
||||
private DateTimeFormatter dtf;
|
||||
|
||||
public JsonInstantSerializer() {
|
||||
dtf = DateTimeFormatter.ISO_INSTANT;
|
||||
}
|
||||
|
||||
public JsonInstantSerializer(String format) {
|
||||
dtf = DateTimeFormatter.ofPattern(format);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Instant src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(dtf.format(src));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Instant deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
return dtf.parse(json.getAsString(), Instant::from);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.flume.utils;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
/**
|
||||
* The {@link JsonLocalDateTimeSerializer} implements gson serializer for Java LocalDateTime.
|
||||
*
|
||||
* @author Jeff James - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class JsonLocalDateTimeSerializer implements JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> {
|
||||
private DateTimeFormatter dtf;
|
||||
|
||||
public JsonLocalDateTimeSerializer() {
|
||||
dtf = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
}
|
||||
|
||||
public JsonLocalDateTimeSerializer(String format) {
|
||||
dtf = DateTimeFormatter.ofPattern(format);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(LocalDateTime src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(dtf.format(src));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable LocalDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
return dtf.parse(json.getAsString(), LocalDateTime::from);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="flume" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>Flume Binding</name>
|
||||
<description>This is the binding for Flume water monitor.</description>
|
||||
<connection>cloud</connection>
|
||||
</addon:addon>
|
@ -0,0 +1,96 @@
|
||||
# add-on
|
||||
|
||||
addon.flume.name = Flume Binding
|
||||
addon.flume.description = This is the binding for Flume water monitor.
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.flume.cloud.label = Flume Cloud Connector
|
||||
thing-type.flume.cloud.description = Flume cloud connector.
|
||||
thing-type.flume.meter-device.label = Flume Meter Device
|
||||
thing-type.flume.meter-device.description = Flume water meter device.
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.flume.cloud.clientId.label = Flume Client ID
|
||||
thing-type.config.flume.cloud.clientId.description = Visit Flume cloud portal to get client ID
|
||||
thing-type.config.flume.cloud.clientSecret.label = Flume Client Secret
|
||||
thing-type.config.flume.cloud.clientSecret.description = Visit Flume cloud portal to get client secret
|
||||
thing-type.config.flume.cloud.password.label = Flume Password
|
||||
thing-type.config.flume.cloud.password.description = Flume cloud portal password
|
||||
thing-type.config.flume.cloud.refreshIntervalCumulative.label = Cumulative Refresh Interval
|
||||
thing-type.config.flume.cloud.refreshIntervalCumulative.description = Minutes between fetching cumulative usage from the cloud service (total cloud fetches is rate-limited to 120/hour)
|
||||
thing-type.config.flume.cloud.refreshIntervalInstanteous.label = Instantaneous Refresh Interval
|
||||
thing-type.config.flume.cloud.refreshIntervalInstanteous.description = Minutes between fetching current flow rate from the cloud service (total cloud fetches is rate-limited to 120/hour)
|
||||
thing-type.config.flume.cloud.username.label = Flume Username
|
||||
thing-type.config.flume.cloud.username.description = Flume cloud portal username
|
||||
thing-type.config.flume.meter-device.id.label = ID
|
||||
thing-type.config.flume.meter-device.id.description = Device ID
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.flume.cumulative-usage.label = Cumulative Used
|
||||
channel-type.flume.cumulative-usage.description = Cumulative water used (volume)
|
||||
channel-type.flume.instant-usage.label = Instant Water Usage
|
||||
channel-type.flume.instant-usage.description = Instantaneous water flow rate (volume / minute)
|
||||
channel-type.flume.last-seen.label = Last Seen
|
||||
channel-type.flume.last-seen.description = Date/Time when device was last seen
|
||||
channel-type.flume.usage-alert.label = Usage Alert
|
||||
channel-type.flume.usage-alert.description = Trigger of a usage alert
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.flume.device.label = Flume Meter Device
|
||||
thing-type.flume.device.description = Flume water meter device.
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.flume.device.id.label = ID
|
||||
thing-type.config.flume.device.id.description = Device ID
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.flume.cumulativeUsage.label = Cumulative Used
|
||||
channel-type.flume.cumulativeUsage.description = Cumulative water used (volume)
|
||||
channel-type.flume.instantUsage.label = Instant Water Usage
|
||||
channel-type.flume.instantUsage.description = Instantaneous water flow rate (volume / minute)
|
||||
channel-type.flume.lastSeen.label = Last Seen
|
||||
channel-type.flume.lastSeen.description = Date/Time when device was last seen
|
||||
channel-type.flume.usageAlert.label = Usage Alert
|
||||
channel-type.flume.usageAlert.description = Trigger of a usage alert
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.flume.cloud.refreshInterval.label = Refresh Interval
|
||||
thing-type.config.flume.cloud.refreshInterval.description = Seconds between fetching values from the cloud service
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.flume.todayUsage.label = Today Used
|
||||
channel-type.flume.todayUsage.description = Amount of water used today (volume)
|
||||
|
||||
# binding
|
||||
|
||||
binding.flume.name = Flume Binding
|
||||
binding.flume.description = This is the binding for flume.
|
||||
|
||||
# thing status description
|
||||
|
||||
offline.cloud-configuration-error = Unable to connect to Flume cloud, please check cloud configuration
|
||||
offline.cloud-connection-issue = Unable to connect to Flume cloud due to connection issues
|
||||
offline.configuration-error.bridge-missing = Flume Cloud Connector bridge must be online
|
||||
offline.device-configuration-error = Flume device configuration is invalid, please check device conviguration
|
||||
|
||||
# api error conditions
|
||||
|
||||
api.invalid-user-credentials = Invalid user credentials, please check configuration
|
||||
api.retrieve-token-fail = Retrieve token fail
|
||||
api.response-fail = API response fail
|
||||
api.response-invalid = API response invalid
|
||||
api.query-fail = API query fail
|
||||
api.rate-limit-exceeded = API rate limit exceeded
|
||||
api.bad-request = API error in request sent to the server
|
||||
|
||||
# misc
|
||||
|
||||
trigger.high-flow-alert = %s triggered at %s. Water has been running for %d minutes averaging %.1f %s every minute.
|
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="flume"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="cloud">
|
||||
<label>Flume Cloud Connector</label>
|
||||
<description>Flume cloud connector.</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="username" type="text">
|
||||
<label>Flume Username</label>
|
||||
<description>Flume cloud portal username</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="password" type="text">
|
||||
<label>Flume Password</label>
|
||||
<context>password</context>
|
||||
<description>Flume cloud portal password</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="clientId" type="text">
|
||||
<label>Flume Client ID</label>
|
||||
<description>Visit Flume cloud portal to get client ID</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="clientSecret" type="text">
|
||||
<label>Flume Client Secret</label>
|
||||
<context>password</context>
|
||||
<description>Visit Flume cloud portal to get client secret</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="refreshIntervalInstanteous" type="integer" min="1" step="1" unit="min">
|
||||
<label>Instantaneous Refresh Interval</label>
|
||||
<description>Minutes between fetching current flow rate from the cloud service (total cloud fetches is rate-limited
|
||||
to 120/hour)</description>
|
||||
<required>false</required>
|
||||
<advanced>true</advanced>
|
||||
<default>1</default>
|
||||
<unitLabel>minutes</unitLabel>
|
||||
</parameter>
|
||||
<parameter name="refreshIntervalCumulative" type="integer" min="1" step="5" unit="min">
|
||||
<label>Cumulative Refresh Interval</label>
|
||||
<description>Minutes between fetching cumulative usage from the cloud service (total cloud fetches is rate-limited
|
||||
to 120/hour)</description>
|
||||
<required>false</required>
|
||||
<advanced>true</advanced>
|
||||
<default>5</default>
|
||||
<unitLabel>minutes</unitLabel>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="flume"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="meter-device">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="cloud"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Flume Meter Device</label>
|
||||
<description>Flume water meter device.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="instant-usage" typeId="instant-usage"/>
|
||||
<channel id="cumulative-usage" typeId="cumulative-usage"/>
|
||||
<channel id="battery-level" typeId="system.battery-level"/>
|
||||
<channel id="last-seen" typeId="last-seen"/>
|
||||
<channel id="low-battery" typeId="system.low-battery"/>
|
||||
<channel id="usage-alert" typeId="usage-alert"/>
|
||||
</channels>
|
||||
|
||||
<representation-property>id</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="id" type="text">
|
||||
<label>ID</label>
|
||||
<description>Device ID</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="cumulative-usage">
|
||||
<item-type>Number:Volume</item-type>
|
||||
<label>Cumulative Used</label>
|
||||
<description>Cumulative water used (volume)</description>
|
||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="instant-usage">
|
||||
<item-type>Number:VolumetricFlowRate</item-type>
|
||||
<label>Instant Water Usage</label>
|
||||
<description>Instantaneous water flow rate (volume / minute)</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="last-seen">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Last Seen</label>
|
||||
<description>Date/Time when device was last seen</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="usage-alert">
|
||||
<kind>trigger</kind>
|
||||
<label>Usage Alert</label>
|
||||
<description>Trigger of a usage alert</description>
|
||||
<event></event>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
@ -149,6 +149,7 @@
|
||||
<module>org.openhab.binding.fenecon</module>
|
||||
<module>org.openhab.binding.fineoffsetweatherstation</module>
|
||||
<module>org.openhab.binding.flicbutton</module>
|
||||
<module>org.openhab.binding.flume</module>
|
||||
<module>org.openhab.binding.fmiweather</module>
|
||||
<module>org.openhab.binding.folderwatcher</module>
|
||||
<module>org.openhab.binding.folding</module>
|
||||
|
Loading…
Reference in New Issue
Block a user