mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[meater] Initial contribution (#13400)
* First version. Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>
This commit is contained in:
parent
73e18424b9
commit
c75485df16
@ -182,6 +182,7 @@
|
||||
/bundles/org.openhab.binding.max/ @marcelrv
|
||||
/bundles/org.openhab.binding.mcd/ @simon-dengler
|
||||
/bundles/org.openhab.binding.mcp23017/ @aogorek
|
||||
/bundles/org.openhab.binding.meater/ @jannegpriv
|
||||
/bundles/org.openhab.binding.mecmeter/ @kaikreuzer
|
||||
/bundles/org.openhab.binding.melcloud/ @lucacalcaterra @paulianttila @thewiep
|
||||
/bundles/org.openhab.binding.meteoalerte/ @clinique
|
||||
|
@ -911,6 +911,11 @@
|
||||
<artifactId>org.openhab.binding.mcp23017</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.meater</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.mecmeter</artifactId>
|
||||
|
13
bundles/org.openhab.binding.meater/NOTICE
Normal file
13
bundles/org.openhab.binding.meater/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
|
146
bundles/org.openhab.binding.meater/README.md
Normal file
146
bundles/org.openhab.binding.meater/README.md
Normal file
@ -0,0 +1,146 @@
|
||||
# MEATER Binding
|
||||
|
||||
This is an openHAB binding for the MEATER probe, MEATER® is a trademark of Apption Labs™ Limited. A Traeger Company.
|
||||
|
||||
This binding uses the MEATER Cloud REST API.
|
||||
|
||||
![Meater+ Probe](doc/meater-plus-side.png)
|
||||
|
||||
|
||||
## Supported Things
|
||||
|
||||
This binding supports the following thing types:
|
||||
|
||||
- meaterapi: Bridge - Communicates with the MEATER Cloud REST API.
|
||||
|
||||
|
||||
- meaterprobe: The MEATER probe - Only support for cloud connected MEATER probes (MEATER Block and MEATER Plus)
|
||||
|
||||
## Discovery
|
||||
|
||||
The preferred way of adding MEATER probe(s) since the probe IDs are not easily found.
|
||||
|
||||
**NOTE**: For The Original MEATER and MEATER Plus you need to have your MEATER app running and the MEATER probe(s) must connected to the cloud (out of the charger box) before you start the discovery.
|
||||
|
||||
|
||||
After the configuration of the Bridge, you need to perform a manual scan and then your MEATER probe(s) will be automatically discovered and placed as a thing(s) in the inbox.
|
||||
|
||||
|
||||
|
||||
## Supported Things and Channels
|
||||
|
||||
### MEATER Bridge
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
| Parameter | Description | Type | Default | Required |
|
||||
|-----------|--------------------------------------------------------------|--------|----------|----------|
|
||||
| email | The email used to login to your MEATER Cloud account | String | NA | yes |
|
||||
| password | The password used to login to your MEATER Cloud account | String | NA | yes |
|
||||
| refresh | Specifies the refresh interval in seconds | Number | 30 | no |
|
||||
|
||||
#### Channels
|
||||
|
||||
The following channels are supported:
|
||||
|
||||
| Channel Type ID | Item Type | Description |
|
||||
|-----------------|-----------|-------------------------------------------------------------------------------------------------|
|
||||
| status | String | Can be used to trigger an instant refresh by sending a `REFRESH` command.|
|
||||
|
||||
### MEATER Probe
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
| Parameter | Description | Type | Default | Required |
|
||||
|-----------|--------------------------------------------------------------|--------|----------|----------|
|
||||
| deviceId | Unique id for your MEATER Probe | String | NA | yes |
|
||||
|
||||
|
||||
|
||||
#### Channels
|
||||
|
||||
| Channel Type ID | Item Type | Description |
|
||||
|-----------------------|--------------------|------------------------------------------------------|
|
||||
| internalTemperature | Number:Temperature | Internal temperature reading of MEATER probe |
|
||||
| ambientTemperature | Number:Temperature | Ambient temperature reading of MEATER probe. If ambient is less than internal, ambient will equal internal |
|
||||
| cookTargetTemperature | Number:Temperature | Target temperature of current cook |
|
||||
| cookPeakTemperature | Number:Temperature | Peak temperature of current cook |
|
||||
| lastConnection | DateTime | Date and time of last probe connection |
|
||||
| cookId | String | Unique cook ID of current cook |
|
||||
| cookName | String | Name of selected meat or user given custom name |
|
||||
| cookState | String | One of Not Started, Configured, Started, Ready For Resting, Resting, Slightly Underdone, Finished, Slightly Overdone, OVERCOOK! |
|
||||
| cookElapsedTime | Number:Time | Time since the start of cook in seconds. Default: 0 |
|
||||
| cookRemainingTime | Number:Time | Remaining time in seconds or UNDEF when unknown. |
|
||||
| cookEstimatedEndTime | DateTime | Date and time of estimated end time for current cook |
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
### Things-file
|
||||
|
||||
````
|
||||
Bridge meater:meaterapi:block "MEATER Block" [email="", password="", refresh=30] {
|
||||
meaterprobe probe1 "Meater Probe 1" [deviceId=""]
|
||||
meaterprobe probe2 "Meater Probe 2" [deviceId=""]
|
||||
meaterprobe probe3 "Meater Probe 3" [deviceId=""]
|
||||
meaterprobe probe4 "Meater Probe 4" [deviceId=""]
|
||||
}
|
||||
````
|
||||
|
||||
### Items-file
|
||||
|
||||
````
|
||||
Number:Temperature Probe1InternalTemperature {channel="meater:meaterprobe:block:probe1:internalTemperature"}
|
||||
Number:Temperature Probe1AmbientTemperature {channel="meater:meaterprobe:block:probe1:ambientTemperature"}
|
||||
String Probe1CookId {channel="meater:meaterprobe:block:probe1:cookId"}
|
||||
String Probe1CookName {channel="meater:meaterprobe:block:probe1:cookName"}
|
||||
String Probe1CookState {channel="meater:meaterprobe:block:probe1:cookState"}
|
||||
Number:Temperature Probe1CookTargetTemperature {channel="meater:meaterprobe:block:probe1:cookTargetTemperature"}
|
||||
Number:Temperature Probe1CookPeakTemperature {channel="meater:meaterprobe:block:probe1:cookPeakTemperature"}
|
||||
Number:Time Probe1CookElapsedTime {channel="meater:meaterprobe:block:probe1:cookElapsedTime"}
|
||||
Number:Time Probe1CookRemainingTime {channel="meater:meaterprobe:block:probe1:cookRemainingTime"}
|
||||
DateTime Probe1CookEstimatedEndTime {channel="meater:meaterprobe:block:probe1:cookEstimatedEndTime"}
|
||||
String Probe1Status {channel="meater:meaterprobe:block:probe1:status"}
|
||||
DateTime Probe1LastConnection {channel="meater:meaterprobe:block:probe1:lastConnection"}
|
||||
|
||||
Number:Temperature Probe2InternalTemperature {channel="meater:meaterprobe:block:probe2:internalTemperature"}
|
||||
Number:Temperature Probe2AmbientTemperature {channel="meater:meaterprobe:block:probe2:ambientTemperature"}
|
||||
String Probe2CookId {channel="meater:meaterprobe:block:probe2:cookId"}
|
||||
String Probe2CookName {channel="meater:meaterprobe:block:probe2:cookName"}
|
||||
String Probe2CookState {channel="meater:meaterprobe:block:probe2:cookState"}
|
||||
Number:Temperature Probe2CookTargetTemperature {channel="meater:meaterprobe:block:probe2:cookTargetTemperature"}
|
||||
Number:Temperature Probe2CookPeakTemperature {channel="meater:meaterprobe:block:probe2:cookPeakTemperature"}
|
||||
Number:Time Probe2CookElapsedTime {channel="meater:meaterprobe:block:probe2:cookElapsedTime"}
|
||||
Number:Time Probe2CookRemainingTime {channel="meater:meaterprobe:block:probe2:cookRemainingTime"}
|
||||
DateTime Probe2CookEstimatedEndTime {channel="meater:meaterprobe:block:probe2:cookEstimatedEndTime"}
|
||||
String Probe2Status {channel="meater:meaterprobe:block:probe2:status"}
|
||||
DateTime Probe2LastConnection {channel="meater:meaterprobe:block:probe2:lastConnection"}
|
||||
|
||||
Number:Temperature Probe3InternalTemperature {channel="meater:meaterprobe:block:probe3:internalTemperature"}
|
||||
Number:Temperature Probe3AmbientTemperature {channel="meater:meaterprobe:block:probe3:ambientTemperature"}
|
||||
String Probe3CookId {channel="meater:meaterprobe:block:probe3:cookId"}
|
||||
String Probe3CookName {channel="meater:meaterprobe:block:probe3:cookName"}
|
||||
String Probe3CookState {channel="meater:meaterprobe:block:probe3:cookState"}
|
||||
Number:Temperature Probe3CookTargetTemperature {channel="meater:meaterprobe:block:probe3:cookTargetTemperature"}
|
||||
Number:Temperature Probe3CookPeakTemperature {channel="meater:meaterprobe:block:probe3:cookPeakTemperature"}
|
||||
Number:Time Probe3CookElapsedTime {channel="meater:meaterprobe:block:probe3:cookElapsedTime"}
|
||||
Number:Time Probe3CookRemainingTime {channel="meater:meaterprobe:block:probe3:cookRemainingTime"}
|
||||
DateTime Probe3CookEstimatedEndTime {channel="meater:meaterprobe:block:probe3:cookEstimatedEndTime"}
|
||||
String Probe3Status {channel="meater:meaterprobe:block:probe3:status"}
|
||||
DateTime Probe3LastConnection {channel="meater:meaterprobe:block:probe3:lastConnection"}
|
||||
|
||||
Number:Temperature Probe4InternalTemperature {channel="meater:meaterprobe:block:probe4:internalTemperature"}
|
||||
Number:Temperature Probe4AmbientTemperature {channel="meater:meaterprobe:block:probe4:ambientTemperature"}
|
||||
String Probe4CookId {channel="meater:meaterprobe:block:probe4:cookId"}
|
||||
String Probe4CookName {channel="meater:meaterprobe:block:probe4:cookName"}
|
||||
String Probe4CookState {channel="meater:meaterprobe:block:probe4:cookState"}
|
||||
Number:Temperature Probe4CookTargetTemperature {channel="meater:meaterprobe:block:probe4:cookTargetTemperature"}
|
||||
Number:Temperature Probe4CookPeakTemperature {channel="meater:meaterprobe:block:probe4:cookPeakTemperature"}
|
||||
Number:Time Probe4CookElapsedTime {channel="meater:meaterprobe:block:probe4:cookElapsedTime"}
|
||||
Number:Time Probe4CookRemainingTime {channel="meater:meaterprobe:block:probe4:cookRemainingTime"}
|
||||
DateTime Probe4CookEstimatedEndTime {channel="meater:meaterprobe:block:probe4:cookEstimatedEndTime"}
|
||||
String Probe4Status {channel="meater:meaterprobe:block:probe4:status"}
|
||||
DateTime Probe4LastConnection {channel="meater:meaterprobe:block:probe4:lastConnection"}
|
||||
````
|
||||
|
||||
|
BIN
bundles/org.openhab.binding.meater/doc/meater-plus-side.png
Normal file
BIN
bundles/org.openhab.binding.meater/doc/meater-plus-side.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 187 KiB |
17
bundles/org.openhab.binding.meater/pom.xml
Normal file
17
bundles/org.openhab.binding.meater/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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.4.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.meater</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Meater Binding</name>
|
||||
|
||||
</project>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.meater-${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-meater" description="Meater Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.meater/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.meater.internal;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link MeaterBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Jan Gustafsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeaterBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "meater";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_MEATER_PROBE = new ThingTypeUID(BINDING_ID, "meaterprobe");
|
||||
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "meaterapi");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_STATUS = "status";
|
||||
public static final String CHANNEL_LAST_CONNECTION = "lastConnection";
|
||||
public static final String CHANNEL_INTERNAL_TEMPERATURE = "internalTemperature";
|
||||
public static final String CHANNEL_AMBIENT_TEMPERATURE = "ambientTemperature";
|
||||
public static final String CHANNEL_COOK_ID = "cookId";
|
||||
public static final String CHANNEL_COOK_NAME = "cookName";
|
||||
public static final String CHANNEL_COOK_STATE = "cookState";
|
||||
public static final String CHANNEL_COOK_TARGET_TEMPERATURE = "cookTargetTemperature";
|
||||
public static final String CHANNEL_COOK_PEAK_TEMPERATURE = "cookPeakTemperature";
|
||||
public static final String CHANNEL_COOK_ELAPSED_TIME = "cookElapsedTime";
|
||||
public static final String CHANNEL_COOK_REMAINING_TIME = "cookRemainingTime";
|
||||
public static final String CHANNEL_COOK_ESTIMATED_END_TIME = "cookEstimatedEndTime";
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE,
|
||||
THING_TYPE_MEATER_PROBE);
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.meater.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link MeaterBridgeConfiguration} class contains fields mapping bridge configuration parameters.
|
||||
*
|
||||
* @author Jan Gustafsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeaterBridgeConfiguration {
|
||||
public String email = "";
|
||||
public String password = "";
|
||||
public int refresh = 30;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.meater.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link MeaterConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Jan Gustafsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeaterConfiguration {
|
||||
|
||||
public static final String DEVICE_ID_LABEL = "deviceId";
|
||||
|
||||
private String deviceId = "";
|
||||
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
}
|
@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.meater.internal.api;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpResponseException;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.meater.internal.MeaterBridgeConfiguration;
|
||||
import org.openhab.binding.meater.internal.dto.MeaterProbeDTO;
|
||||
import org.openhab.binding.meater.internal.dto.MeaterProbeDTO.Device;
|
||||
import org.openhab.binding.meater.internal.exceptions.MeaterAuthenticationException;
|
||||
import org.openhab.binding.meater.internal.exceptions.MeaterException;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* The {@link MeaterRestAPI} class defines the MEATER REST API
|
||||
*
|
||||
* @author Jan Gustafsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeaterRestAPI {
|
||||
private static final String API_ENDPOINT = "https://public-api.cloud.meater.com/v1/";
|
||||
private static final String JSON_CONTENT_TYPE = "application/json";
|
||||
private static final String LOGIN = "login";
|
||||
private static final String DEVICES = "devices";
|
||||
private static final int MAX_RETRIES = 3;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MeaterRestAPI.class);
|
||||
private final Gson gson;
|
||||
private final HttpClient httpClient;
|
||||
private final MeaterBridgeConfiguration configuration;
|
||||
private String authToken = "";
|
||||
private LocaleProvider localeProvider;
|
||||
|
||||
public MeaterRestAPI(MeaterBridgeConfiguration configuration, Gson gson, HttpClient httpClient,
|
||||
LocaleProvider localeProvider) {
|
||||
this.gson = gson;
|
||||
this.configuration = configuration;
|
||||
this.httpClient = httpClient;
|
||||
this.localeProvider = localeProvider;
|
||||
}
|
||||
|
||||
public boolean refresh(Map<String, MeaterProbeDTO.Device> meaterProbeThings) {
|
||||
try {
|
||||
MeaterProbeDTO dto = getDevices(MeaterProbeDTO.class);
|
||||
if (dto != null) {
|
||||
List<Device> devices = dto.getData().getDevices();
|
||||
if (devices != null) {
|
||||
if (!devices.isEmpty()) {
|
||||
for (Device meaterProbe : devices) {
|
||||
meaterProbeThings.put(meaterProbe.id, meaterProbe);
|
||||
}
|
||||
} else {
|
||||
meaterProbeThings.clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (MeaterException e) {
|
||||
logger.warn("Failed to refresh! {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void login() throws MeaterException {
|
||||
try {
|
||||
// Login
|
||||
String json = "{ \"email\": \"" + configuration.email + "\", \"password\": \"" + configuration.password
|
||||
+ "\" }";
|
||||
Request request = httpClient.newRequest(API_ENDPOINT + LOGIN).method(HttpMethod.POST);
|
||||
request.header(HttpHeader.ACCEPT, JSON_CONTENT_TYPE);
|
||||
request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
|
||||
request.content(new StringContentProvider(json), JSON_CONTENT_TYPE);
|
||||
|
||||
logger.trace("{}.", request.toString());
|
||||
|
||||
ContentResponse httpResponse = request.send();
|
||||
if (!HttpStatus.isSuccess(httpResponse.getStatus())) {
|
||||
throw new MeaterException("Failed to login " + httpResponse.getContentAsString());
|
||||
}
|
||||
// Fetch JWT
|
||||
json = httpResponse.getContentAsString();
|
||||
JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
|
||||
JsonObject childObject = jsonObject.getAsJsonObject("data");
|
||||
JsonElement tokenJson = childObject.get("token");
|
||||
if (tokenJson != null) {
|
||||
this.authToken = tokenJson.getAsString();
|
||||
} else {
|
||||
throw new MeaterException("Token is not present in the JSON response");
|
||||
}
|
||||
} catch (TimeoutException | ExecutionException | JsonParseException e) {
|
||||
throw new MeaterException(e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new MeaterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getFromApi(String uri) throws MeaterException {
|
||||
try {
|
||||
for (int i = 0; i < MAX_RETRIES; i++) {
|
||||
try {
|
||||
Request request = httpClient.newRequest(API_ENDPOINT + uri).method(HttpMethod.GET);
|
||||
request.header(HttpHeader.AUTHORIZATION, "Bearer " + authToken);
|
||||
request.header(HttpHeader.ACCEPT, JSON_CONTENT_TYPE);
|
||||
request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
|
||||
request.header(HttpHeader.ACCEPT_LANGUAGE, localeProvider.getLocale().getLanguage());
|
||||
|
||||
ContentResponse response = request.send();
|
||||
String content = response.getContentAsString();
|
||||
logger.trace("API response: {}", content);
|
||||
|
||||
if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
|
||||
// This will currently not happen because "WWW-Authenticate" header is missing; see below.
|
||||
logger.debug("getFromApi failed, authentication failed, HTTP status: 401");
|
||||
throw new MeaterAuthenticationException("Authentication failed");
|
||||
} else if (!HttpStatus.isSuccess(response.getStatus())) {
|
||||
logger.debug("getFromApi failed, HTTP status: {}", response.getStatus());
|
||||
throw new MeaterException("Failed to fetch from API!");
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
} catch (TimeoutException e) {
|
||||
logger.debug("TimeoutException error in get: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
throw new MeaterException("Failed to fetch from API!");
|
||||
} catch (ExecutionException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause != null && cause instanceof HttpResponseException) {
|
||||
Response response = ((HttpResponseException) cause).getResponse();
|
||||
if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
|
||||
/*
|
||||
* When contextId is not valid, the service will respond with HTTP code 401 without
|
||||
* any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
|
||||
* HttpResponseException. We need to handle this in order to attempt
|
||||
* reauthentication.
|
||||
*/
|
||||
logger.debug("getFromApi failed, authentication failed, HTTP status: 401");
|
||||
throw new MeaterAuthenticationException("Authentication failed");
|
||||
}
|
||||
}
|
||||
throw new MeaterException(e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new MeaterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable <T> T getDevices(Class<T> dto) throws MeaterException {
|
||||
String uri = DEVICES;
|
||||
String json = "";
|
||||
|
||||
if (authToken.isEmpty()) {
|
||||
login();
|
||||
}
|
||||
|
||||
try {
|
||||
json = getFromApi(uri);
|
||||
} catch (MeaterAuthenticationException e) {
|
||||
logger.debug("getFromApi failed {}", e.getMessage());
|
||||
this.authToken = "";
|
||||
login();
|
||||
json = getFromApi(uri);
|
||||
}
|
||||
|
||||
if (json.isEmpty()) {
|
||||
throw new MeaterException("JSON from API is empty!");
|
||||
} else {
|
||||
try {
|
||||
return gson.fromJson(json, dto);
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new MeaterException("Error parsing JSON", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.meater.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.meater.internal.MeaterBindingConstants.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.meater.internal.MeaterConfiguration;
|
||||
import org.openhab.binding.meater.internal.handler.MeaterBridgeHandler;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
|
||||
/**
|
||||
* The {@link MeaterDiscoveryService} searches for available
|
||||
* Meater probes discoverable through MEATER REST API.
|
||||
*
|
||||
* @author Jan Gustafsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeaterDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
|
||||
private static final int SEARCH_TIME = 2;
|
||||
private @Nullable MeaterBridgeHandler handler;
|
||||
|
||||
public MeaterDiscoveryService() {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, SEARCH_TIME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
if (handler instanceof MeaterBridgeHandler) {
|
||||
this.handler = (MeaterBridgeHandler) handler;
|
||||
i18nProvider = ((MeaterBridgeHandler) handler).getI18nProvider();
|
||||
localeProvider = ((MeaterBridgeHandler) handler).getLocaleProvider();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activate(@Nullable Map<String, Object> configProperties) {
|
||||
super.activate(configProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
MeaterBridgeHandler bridgeHandler = this.handler;
|
||||
if (bridgeHandler != null) {
|
||||
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
|
||||
bridgeHandler.getMeaterThings().entrySet().stream().forEach(thing -> {
|
||||
thingDiscovered(
|
||||
DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_MEATER_PROBE, bridgeUID, thing.getKey()))
|
||||
.withLabel("@text/discovery.probe.label").withBridge(bridgeUID)
|
||||
.withProperty(MeaterConfiguration.DEVICE_ID_LABEL, thing.getKey())
|
||||
.withRepresentationProperty(MeaterConfiguration.DEVICE_ID_LABEL).build());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.meater.internal.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link MeaterProbeDTO} class defines the DTO for the Meater probe.
|
||||
*
|
||||
* @author Jan Gustafsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeaterProbeDTO {
|
||||
|
||||
public String status = "";
|
||||
public long statusCode;
|
||||
public Data data = new Data();
|
||||
public Meta meta = new Meta();
|
||||
|
||||
public Data getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public class Data {
|
||||
|
||||
@Nullable
|
||||
public List<Device> devices;
|
||||
|
||||
public @Nullable List<Device> getDevices() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
public @Nullable Device getDevice(String id) {
|
||||
List<Device> localDevices = devices;
|
||||
if (localDevices != null) {
|
||||
for (Device meaterProbe : localDevices) {
|
||||
if (id.equals(meaterProbe.id)) {
|
||||
return meaterProbe;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class Meta {
|
||||
}
|
||||
|
||||
public class Device {
|
||||
public String id = "";
|
||||
public Temperature temperature = new Temperature();
|
||||
public @Nullable Cook cook = new Cook();
|
||||
@SerializedName("updated_at")
|
||||
private long lastConnection;
|
||||
|
||||
public @Nullable Instant getLastConnection() {
|
||||
if (lastConnection > 0) {
|
||||
return Instant.ofEpochSecond(lastConnection);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class Cook {
|
||||
public String id = "";
|
||||
public String name = "";
|
||||
public String state = "";
|
||||
public TemperatureCook temperature = new TemperatureCook();
|
||||
public Time time = new Time();
|
||||
}
|
||||
|
||||
public class TemperatureCook {
|
||||
public double target;
|
||||
public double peak;
|
||||
}
|
||||
|
||||
public class Temperature {
|
||||
public double internal;
|
||||
public double ambient;
|
||||
}
|
||||
|
||||
public class Time {
|
||||
public long elapsed;
|
||||
public long remaining;
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.meater.internal.exceptions;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* {@link MeaterAuthenticationException} is used when there is an authentication exception with MEATER REST API.
|
||||
*
|
||||
* @author Jan Gustafsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeaterAuthenticationException extends MeaterException {
|
||||
|
||||
private static final long serialVersionUID = 2543564118231301158L;
|
||||
|
||||
public MeaterAuthenticationException(Exception source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
public MeaterAuthenticationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public MeaterAuthenticationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.meater.internal.exceptions;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* {@link MeaterException} is used when there is exception communicating with MEATER REST API.
|
||||
*
|
||||
* @author Jan Gustafsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeaterException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 2543564118231301158L;
|
||||
|
||||
public MeaterException(Exception source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
public MeaterException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public MeaterException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getMessage() {
|
||||
Throwable throwable = getCause();
|
||||
if (throwable != null) {
|
||||
String localMessage = throwable.getMessage();
|
||||
if (localMessage != null) {
|
||||
return localMessage;
|
||||
}
|
||||
}
|
||||
return super.getMessage();
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.meater.internal.handler;
|
||||
|
||||
import static org.openhab.binding.meater.internal.MeaterBindingConstants.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.meater.internal.MeaterBridgeConfiguration;
|
||||
import org.openhab.binding.meater.internal.api.MeaterRestAPI;
|
||||
import org.openhab.binding.meater.internal.discovery.MeaterDiscoveryService;
|
||||
import org.openhab.binding.meater.internal.dto.MeaterProbeDTO;
|
||||
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.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
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;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link MeaterBridgeHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jan Gustafsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeaterBridgeHandler extends BaseBridgeHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MeaterBridgeHandler.class);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
|
||||
|
||||
private final Gson gson;
|
||||
private final HttpClient httpClient;
|
||||
private final TranslationProvider i18nProvider;
|
||||
private final LocaleProvider localeProvider;
|
||||
private final Map<String, MeaterProbeDTO.Device> meaterProbeThings = new ConcurrentHashMap<>();
|
||||
|
||||
private int refreshTimeInSeconds = 300;
|
||||
private @Nullable MeaterRestAPI api;
|
||||
private @Nullable ScheduledFuture<?> refreshJob;
|
||||
|
||||
public MeaterBridgeHandler(Bridge bridge, HttpClient httpClient, Gson gson, TranslationProvider i18nProvider,
|
||||
LocaleProvider localeProvider) {
|
||||
super(bridge);
|
||||
this.httpClient = httpClient;
|
||||
this.gson = gson;
|
||||
this.i18nProvider = i18nProvider;
|
||||
this.localeProvider = localeProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
MeaterBridgeConfiguration config = getConfigAs(MeaterBridgeConfiguration.class);
|
||||
|
||||
api = new MeaterRestAPI(config, gson, httpClient, localeProvider);
|
||||
refreshTimeInSeconds = config.refresh;
|
||||
|
||||
if (config.email.isBlank() || config.password.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/config.missing-username-password.description");
|
||||
} else {
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
scheduler.execute(() -> {
|
||||
startAutomaticRefresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, MeaterProbeDTO.Device> getMeaterThings() {
|
||||
return meaterProbeThings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Collections.singleton(MeaterDiscoveryService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopAutomaticRefresh();
|
||||
meaterProbeThings.clear();
|
||||
}
|
||||
|
||||
private boolean refreshAndUpdateStatus() {
|
||||
MeaterRestAPI localAPI = api;
|
||||
if (localAPI != null) {
|
||||
if (localAPI.refresh(meaterProbeThings)) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
getThing().getThings().stream().forEach(thing -> {
|
||||
MeaterHandler handler = (MeaterHandler) thing.getHandler();
|
||||
if (handler != null) {
|
||||
handler.update();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void startAutomaticRefresh() {
|
||||
ScheduledFuture<?> refreshJob = this.refreshJob;
|
||||
if (refreshJob == null || refreshJob.isCancelled()) {
|
||||
this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshAndUpdateStatus, 0, refreshTimeInSeconds,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopAutomaticRefresh() {
|
||||
ScheduledFuture<?> refreshJob = this.refreshJob;
|
||||
if (refreshJob != null) {
|
||||
refreshJob.cancel(true);
|
||||
this.refreshJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.debug("Command received: {}", command);
|
||||
if (command instanceof RefreshType) {
|
||||
if (channelUID.getId().equals(CHANNEL_STATUS)) {
|
||||
logger.debug("Refresh command on status channel {} will trigger instant refresh", channelUID);
|
||||
refreshAndUpdateStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TranslationProvider getI18nProvider() {
|
||||
return i18nProvider;
|
||||
}
|
||||
|
||||
public LocaleProvider getLocaleProvider() {
|
||||
return localeProvider;
|
||||
}
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.meater.internal.handler;
|
||||
|
||||
import static org.openhab.binding.meater.internal.MeaterBindingConstants.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.meater.internal.MeaterConfiguration;
|
||||
import org.openhab.binding.meater.internal.dto.MeaterProbeDTO.Cook;
|
||||
import org.openhab.binding.meater.internal.dto.MeaterProbeDTO.Device;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* The {@link MeaterHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Jan Gustafsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MeaterHandler extends BaseThingHandler {
|
||||
|
||||
private String deviceId = "";
|
||||
private TimeZoneProvider timeZoneProvider;
|
||||
|
||||
public MeaterHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
|
||||
super(thing);
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
deviceId = getConfigAs(MeaterConfiguration.class).getDeviceId();
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
scheduler.execute(() -> {
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
public void update() {
|
||||
Device meaterProbe = getMeaterProbe();
|
||||
if (meaterProbe != null) {
|
||||
update(meaterProbe);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/offline.communication-error.description");
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable Device getMeaterProbe() {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
|
||||
MeaterBridgeHandler bridgeHandler = (MeaterBridgeHandler) bridge.getHandler();
|
||||
if (bridgeHandler != null) {
|
||||
return bridgeHandler.getMeaterThings().get(deviceId);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void update(Device meaterProbe) {
|
||||
// Update all channels from the updated data
|
||||
getThing().getChannels().stream().map(Channel::getUID).filter(channelUID -> isLinked(channelUID))
|
||||
.forEach(channelUID -> {
|
||||
State state = getValue(channelUID.getId(), meaterProbe);
|
||||
updateState(channelUID, state);
|
||||
});
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
private State getValue(String channelId, Device meaterProbe) {
|
||||
Cook cook = meaterProbe.cook;
|
||||
switch (channelId) {
|
||||
case CHANNEL_INTERNAL_TEMPERATURE:
|
||||
return new QuantityType<Temperature>(meaterProbe.temperature.internal, SIUnits.CELSIUS);
|
||||
case CHANNEL_AMBIENT_TEMPERATURE:
|
||||
return new QuantityType<Temperature>(meaterProbe.temperature.ambient, SIUnits.CELSIUS);
|
||||
case CHANNEL_COOK_TARGET_TEMPERATURE:
|
||||
if (cook != null) {
|
||||
return new QuantityType<Temperature>(cook.temperature.target, SIUnits.CELSIUS);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_COOK_PEAK_TEMPERATURE:
|
||||
if (cook != null) {
|
||||
return new QuantityType<Temperature>(cook.temperature.peak, SIUnits.CELSIUS);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_COOK_ELAPSED_TIME:
|
||||
if (cook != null) {
|
||||
return new QuantityType<>(cook.time.elapsed, Units.SECOND);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_COOK_REMAINING_TIME:
|
||||
if (cook != null) {
|
||||
return cook.time.remaining == -1 ? UnDefType.UNDEF
|
||||
: new QuantityType<>(cook.time.remaining, Units.SECOND);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_COOK_ID:
|
||||
if (cook != null) {
|
||||
return new StringType(cook.id);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_COOK_NAME:
|
||||
if (cook != null) {
|
||||
return new StringType(cook.name);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_COOK_STATE:
|
||||
if (cook != null) {
|
||||
return new StringType(cook.state);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_LAST_CONNECTION:
|
||||
Instant instant = meaterProbe.getLastConnection();
|
||||
if (instant != null) {
|
||||
return new DateTimeType(ZonedDateTime.ofInstant(instant, timeZoneProvider.getTimeZone()));
|
||||
}
|
||||
break;
|
||||
case CHANNEL_COOK_ESTIMATED_END_TIME:
|
||||
if (cook != null) {
|
||||
if (cook.time.remaining > -1) {
|
||||
return new DateTimeType(
|
||||
ZonedDateTime.now(timeZoneProvider.getTimeZone()).plusSeconds(cook.time.remaining));
|
||||
}
|
||||
}
|
||||
}
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.meater.internal.handler;
|
||||
|
||||
import static org.openhab.binding.meater.internal.MeaterBindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
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;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link MeaterHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Jan Gustafsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.meater", service = ThingHandlerFactory.class)
|
||||
public class MeaterHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_MEATER_PROBE,
|
||||
THING_TYPE_BRIDGE);
|
||||
|
||||
private final Gson gson;
|
||||
private final HttpClient httpClient;
|
||||
private final TimeZoneProvider timeZoneProvider;
|
||||
private final TranslationProvider i18nProvider;
|
||||
private final LocaleProvider localeProvider;
|
||||
|
||||
@Activate
|
||||
public MeaterHandlerFactory(@Reference TimeZoneProvider timeZoneProvider,
|
||||
final @Reference TranslationProvider i18nProvider, @Reference LocaleProvider localeProvider,
|
||||
@Reference HttpClientFactory httpClientFactory) {
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
this.i18nProvider = i18nProvider;
|
||||
this.localeProvider = localeProvider;
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
this.gson = new Gson();
|
||||
}
|
||||
|
||||
@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_MEATER_PROBE.equals(thingTypeUID)) {
|
||||
return new MeaterHandler(thing, timeZoneProvider);
|
||||
} else if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
|
||||
return new MeaterBridgeHandler((Bridge) thing, httpClient, gson, i18nProvider, localeProvider);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="meater" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>MEATER Binding</name>
|
||||
<description>This is the binding for MEATER probe.</description>
|
||||
|
||||
</binding:binding>
|
@ -0,0 +1,63 @@
|
||||
# binding
|
||||
|
||||
binding.meater.name = MEATER Binding
|
||||
binding.meater.description = This is the binding for MEATER probe.
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.meater.meaterapi.label = MEATER Cloud REST API
|
||||
thing-type.meater.meaterapi.description = This bridge represents the MEATER Cloud REST API connector
|
||||
thing-type.meater.meaterprobe.label = MEATER Probe
|
||||
thing-type.meater.meaterprobe.description = This thing represents a MEATER Probe
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.meater.meaterapi.email.label = Email
|
||||
thing-type.config.meater.meaterapi.email.description = The email used to login to MEATER Cloud account
|
||||
thing-type.config.meater.meaterapi.password.label = Password
|
||||
thing-type.config.meater.meaterapi.password.description = The password used to login to MEATER Cloud account
|
||||
thing-type.config.meater.meaterapi.refresh.label = Refresh Interval
|
||||
thing-type.config.meater.meaterapi.refresh.description = Specifies the refresh interval in seconds
|
||||
thing-type.config.meater.meaterprobe.deviceId.label = Device Id
|
||||
thing-type.config.meater.meaterprobe.deviceId.description = Unique id for your MEATER Probe
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.meater.ambientTemperature.label = Probe Ambient Temperature
|
||||
channel-type.meater.ambientTemperature.description = Ambient temperature reading of MEATER Probe. If ambient is less than internal, ambient will equal internal
|
||||
channel-type.meater.cookElapsedTime.label = Cook Elapsed Time
|
||||
channel-type.meater.cookElapsedTime.description = Time since the start of cook in seconds. Default: 0
|
||||
channel-type.meater.cookEstimatedEndTime.label = Cook Estimated End Time
|
||||
channel-type.meater.cookEstimatedEndTime.description = Date and time of estimated end time for current cook
|
||||
channel-type.meater.cookEstimatedEndTime.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM
|
||||
channel-type.meater.cookId.label = Current Cook ID
|
||||
channel-type.meater.cookId.description = Unique cook ID of current cook
|
||||
channel-type.meater.cookName.label = Current Cook Name
|
||||
channel-type.meater.cookName.description = Name of selected meat in your language or user given custom name
|
||||
channel-type.meater.cookPeakTemperature.label = Current Cook Peak Temperature
|
||||
channel-type.meater.cookPeakTemperature.description = Peak temperature of current cook
|
||||
channel-type.meater.cookRemainingTime.label = Cook Remaining Time
|
||||
channel-type.meater.cookRemainingTime.description = Remaining time in seconds or UNDEF when unknown
|
||||
channel-type.meater.cookState.label = Current Cook State
|
||||
channel-type.meater.cookState.description = One of Not Started, Configured, Started, Ready For Resting, Resting, Slightly Underdone, Finished, Slightly Overdone, OVERCOOK!
|
||||
channel-type.meater.cookTargetTemperature.label = Current Cook Target Temperature
|
||||
channel-type.meater.cookTargetTemperature.description = Target temperature of current cook
|
||||
channel-type.meater.internalTemperature.label = Probe Internal Temperature
|
||||
channel-type.meater.internalTemperature.description = Internal temperature reading of MEATER Probe
|
||||
channel-type.meater.lastConnection.label = Last Probe Connection
|
||||
channel-type.meater.lastConnection.description = Date and time of last probe connection
|
||||
channel-type.meater.lastConnection.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM
|
||||
channel-type.meater.status.label = Status
|
||||
channel-type.meater.status.description = Can be used to trigger an instant refresh
|
||||
|
||||
# thing types config
|
||||
|
||||
config.missing-username-password.description = Configuration of username and password is mandatory
|
||||
|
||||
# thing status descriptions
|
||||
|
||||
offline.communication-error.description = MEATER Probe is offline
|
||||
|
||||
# discovery result
|
||||
|
||||
discovery.probe.label = MEATER Probe
|
@ -0,0 +1,165 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="meater"
|
||||
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="meaterapi">
|
||||
<label>MEATER Cloud REST API</label>
|
||||
<description>This bridge represents the MEATER Cloud REST API connector</description>
|
||||
|
||||
<channels>
|
||||
<channel id="status" typeId="status"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="email" type="text" required="true">
|
||||
<label>Email</label>
|
||||
<description>The email used to login to MEATER Cloud account</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="true">
|
||||
<label>Password</label>
|
||||
<context>password</context>
|
||||
<description>The password used to login to MEATER Cloud account</description>
|
||||
</parameter>
|
||||
<parameter name="refresh" type="integer" min="10" unit="s">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Specifies the refresh interval in seconds</description>
|
||||
<default>30</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
<thing-type id="meaterprobe">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="meaterapi"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>MEATER Probe</label>
|
||||
<description>This thing represents a MEATER Probe</description>
|
||||
|
||||
<channels>
|
||||
<channel id="internalTemperature" typeId="internalTemperature"/>
|
||||
<channel id="ambientTemperature" typeId="ambientTemperature"/>
|
||||
<channel id="cookId" typeId="cookId"/>
|
||||
<channel id="cookName" typeId="cookName"/>
|
||||
<channel id="cookState" typeId="cookState"/>
|
||||
<channel id="cookTargetTemperature" typeId="cookTargetTemperature"/>
|
||||
<channel id="cookPeakTemperature" typeId="cookPeakTemperature"/>
|
||||
<channel id="cookElapsedTime" typeId="cookElapsedTime"/>
|
||||
<channel id="cookRemainingTime" typeId="cookRemainingTime"/>
|
||||
<channel id="cookEstimatedEndTime" typeId="cookEstimatedEndTime"/>
|
||||
<channel id="lastConnection" typeId="lastConnection"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">Apption Labs</property>
|
||||
<property name="model">MEATER</property>
|
||||
</properties>
|
||||
|
||||
<representation-property>deviceId</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="deviceId" type="text" required="true">
|
||||
<label>Device Id</label>
|
||||
<description>Unique id for your MEATER Probe</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="status">
|
||||
<item-type>String</item-type>
|
||||
<label>Status</label>
|
||||
<description>Can be used to trigger an instant refresh</description>
|
||||
<state readOnly="true" pattern="%s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="lastConnection">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Last Probe Connection</label>
|
||||
<description>Date and time of last probe connection</description>
|
||||
<category>Time</category>
|
||||
<state readOnly="true" pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="cookElapsedTime">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Cook Elapsed Time</label>
|
||||
<description>Time since the start of cook in seconds. Default: 0</description>
|
||||
<category>Time</category>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="cookRemainingTime">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Cook Remaining Time</label>
|
||||
<description>Remaining time in seconds or UNDEF when unknown</description>
|
||||
<category>Time</category>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="cookEstimatedEndTime">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Cook Estimated End Time</label>
|
||||
<description>Date and time of estimated end time for current cook</description>
|
||||
<category>Time</category>
|
||||
<state readOnly="true" pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="internalTemperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Probe Internal Temperature</label>
|
||||
<description>Internal temperature reading of MEATER Probe</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ambientTemperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Probe Ambient Temperature</label>
|
||||
<description>Ambient temperature reading of MEATER Probe. If ambient is less than internal, ambient will equal
|
||||
internal</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="cookTargetTemperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Current Cook Target Temperature</label>
|
||||
<description>Target temperature of current cook</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="cookPeakTemperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Current Cook Peak Temperature</label>
|
||||
<description>Peak temperature of current cook</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="cookId">
|
||||
<item-type>String</item-type>
|
||||
<label>Current Cook ID</label>
|
||||
<description>Unique cook ID of current cook</description>
|
||||
<state readOnly="true" pattern="%s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="cookName">
|
||||
<item-type>String</item-type>
|
||||
<label>Current Cook Name</label>
|
||||
<description>Name of selected meat in your language or user given custom name</description>
|
||||
<state readOnly="true" pattern="%s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="cookState">
|
||||
<item-type>String</item-type>
|
||||
<label>Current Cook State</label>
|
||||
<description>One of Not Started, Configured, Started, Ready For Resting, Resting, Slightly Underdone, Finished,
|
||||
Slightly Overdone, OVERCOOK!</description>
|
||||
<state readOnly="true" pattern="%s"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -216,6 +216,7 @@
|
||||
<module>org.openhab.binding.max</module>
|
||||
<module>org.openhab.binding.mcd</module>
|
||||
<module>org.openhab.binding.mcp23017</module>
|
||||
<module>org.openhab.binding.meater</module>
|
||||
<module>org.openhab.binding.mecmeter</module>
|
||||
<module>org.openhab.binding.melcloud</module>
|
||||
<module>org.openhab.binding.mercedesme</module>
|
||||
|
Loading…
Reference in New Issue
Block a user