]> git.basschouten.com Git - openhab-addons.git/commitdiff
[warmup] Initial contribution (#8562)
authorJames Melville <jamesmelville@gmail.com>
Thu, 13 May 2021 13:37:05 +0000 (14:37 +0100)
committerGitHub <noreply@github.com>
Thu, 13 May 2021 13:37:05 +0000 (15:37 +0200)
Signed-off-by: James Melville <jamesmelville@gmail.com>
31 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.warmup/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.warmup/README.md [new file with mode: 0644]
bundles/org.openhab.binding.warmup/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/WarmupBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/api/MyWarmupApi.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/api/MyWarmupApiException.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/discovery/WarmupDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/MyWarmupAccountHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/MyWarmupConfigurationDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/RoomConfigurationDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/RoomHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupRefreshListener.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthRequestDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthRequestDataDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseDataDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseStatusDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/DeviceDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/LocationDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/QueryDataDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/QueryResponseDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/RoomDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/UserDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index bd3f9741e401b3dcdf16b77d73f325777d6efa7c..16a0fb25e1b2fa023c1145242e14fe63f5aba235 100644 (file)
 /bundles/org.openhab.binding.vigicrues/ @clinique
 /bundles/org.openhab.binding.vitotronic/ @steand
 /bundles/org.openhab.binding.volvooncall/ @clinique
+/bundles/org.openhab.binding.warmup/ @jamesmelville
 /bundles/org.openhab.binding.weathercompany/ @mhilbush
 /bundles/org.openhab.binding.weatherunderground/ @lolodomo
 /bundles/org.openhab.binding.webthing/ @grro
index b163c68bdbe78c1155a0d95c370e9a57f59aa593..57f14d56c82d7fbcf9215e4164ad9cf7058ed31d 100644 (file)
       <artifactId>org.openhab.binding.volvooncall</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.warmup</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.weathercompany</artifactId>
diff --git a/bundles/org.openhab.binding.warmup/NOTICE b/bundles/org.openhab.binding.warmup/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.warmup/README.md b/bundles/org.openhab.binding.warmup/README.md
new file mode 100644 (file)
index 0000000..bc3f01f
--- /dev/null
@@ -0,0 +1,112 @@
+# Warmup Binding
+
+This binding integrates the Warmup 4iE Thermostat https://www.warmup.co.uk/thermostats/smart/4ie-underfloor-heating, via the API at https://my.warmup.com/.
+
+Any Warmup 4iE device(s) must be registered at https://my.warmup.com/ prior to usage.
+
+This API is not known to be documented publicly. 
+The binding api implementation has been derived from the implementations at https://github.com/alyc100/SmartThingsPublic/blob/master/devicetypes/alyc100/warmup-4ie.src/warmup-4ie.groovy and https://github.com/alex-0103/warmup4IE/blob/master/warmup4ie/warmup4ie.py, and enhanced by inspecting the GraphQL endpoint.
+
+## Supported Things
+
+The Warmup binding supports the following thing types:
+
+| Bridge         | Label             | Description                                                                            |
+|----------------|-------------------|----------------------------------------------------------------------------------------|
+| `my-warmup`    | My Warmup Account | The account credentials for my.warmup.com which acts as an API to the Warmup device(s) |
+
+| Thing    | Label | Description                                                                                                          |
+|----------|-------|----------------------------------------------------------------------------------------------------------------------|
+| `room`   | Room  | A room containing an individual Warmup 4iE device which is a WiFi connected device which controls a heating circuit. |
+
+### Room
+
+The device is optimised for controlling underfloor heating (electric or hydronic), although it can also control central heating circuits.
+The device reports the temperature from one of two thermostats, either a floor temperature probe or the air temperature at the device.
+The separate temperatures do not appear to be reported through the API. It appears to be possible to configure two devices in a primary / secondary configuration, but it is not clear how this might be represented by the API and hasn't been implemented.
+
+## Discovery
+
+Once credentials are successfully added to the bridge, any rooms (devices) detected will be added as things to the inbox.
+
+## Thing Configuration
+
+### My Warmup Account
+
+| config parameter | type    | description                                     | required | default |
+|------------------|---------|-------------------------------------------------|----------|---------|
+| username         | String  | Username for my.warmup.com                      | true     |         |
+| password         | String  | Password for my.warmup.com                      | true     |         |
+| refreshInterval  | Integer | Interval in seconds between automatic refreshes | true     | 300     |
+
+### Room
+
+Rooms are configured automatically with a Serial Number on discovery, or can be added manually using the "Device Number" from the device, excluding the last 3 characters. The only supported temperature change is an override, through a default duration configured on the thing. This defaults to 60 minutes.
+
+| config parameter | type    | description                                                        | required | default |
+|------------------|---------|--------------------------------------------------------------------|----------|---------|
+| serialNumber     | String  | Device Serial Number, excluding last 3 characters                  | true     |         |
+| overrideDuration | Integer | Duration in minutes of override when target temperature is changed | true     | 60      |
+
+
+## Channels
+
+| channel             | type               | description                                                                                                                                  | read only |
+|---------------------|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------|-----------|
+| currentTemperature  | Number:Temperature | Currently reported temperature                                                                                                               | true      |
+| targetTemperature   | Number:Temperature | Target temperature                                                                                                                           | false     |
+| overrideRemaining   | Number:Time        | Duration remaining of the configured override                                                                                                | true      |
+| runMode             | String             | Current operating mode of the thermostat, options listed below                                                                               | true      |
+| frostProtectionMode | Switch             | Toggles between the "Frost Protection" run mode and the previously configured "active" run mode (known options are either Fixed or Schedule) | false     |
+
+
+### Run Mode Statuses
+
+These run mode statuses are defined for the API. The descriptions are based on inspection of the device behaviour and are not sourced from documentation.
+
+| api value  | ui name          | description                                                                     |
+|------------|------------------|---------------------------------------------------------------------------------|
+| not_set    | Not Set          | Unknown                                                                         |
+| off        | Off              | Device turned off                                                               |
+| schedule   | Schedule         | Device target temperature running to a programmed schedule                      |
+| override   | Override         | Target temperature overridden for the remaining duration in overrideRemaining   |
+| fixed      | Fixed            | Device target temperature set to a constant fixed value                         |
+| anti_frost | Frost Protection | Device target temperature set to 7°C                                            |
+| holiday    | Holiday          | Device target temperature set to a constant fixed value for duration of holiday |
+| fil_pilote | Fil Pilote       | Unknown                                                                         |
+| gradual    | Gradual          | Unknown                                                                         |
+| relay      | Relay            | Unknown                                                                         |
+| previous   | Previous         | Unknown                                                                         |
+
+## Full Example
+
+### .things file
+
+```
+Bridge warmup:my-warmup:MyWarmup [ username="test@example.com", password="test", refreshInterval=300 ]
+{
+    room    bathroom    "Home - Bathroom"   [ serialNumber="AABBCCDDEEFF", overrideDuration=60 ]
+}
+```
+
+### .items file
+
+```
+Number:Temperature bathroom_temperature "Temperature [%.1f °C]" <temperature> (GF_Bathroom, Temperature)    ["Temperature"] {channel="warmup:room:MyWarmup:bathroom:currentTemperature"}
+Number:Temperature bathroom_setpoint    "Set Point [%.1f °C]" <temperature> (GF_Bathroom) ["Set Point"] {channel="warmup:room:MyWarmup:bathroom:targetTemperature"}
+Number:Time bathroom_overrideRemaining  "Override Remaining [%d minutes]" (GF_Bathroom) {channel="warmup:room:MyWarmup:bathroom:overrideRemaining"}
+String bathroom_runMode "Run Mode [%s]" (GF_Bathroom) {channel="warmup:room:MyWarmup:bathroom:runMode"}
+Switch bathroom_frostProtection "Frost Protection Mode" (GF_Bathroom) {channel="warmup:room:MyWarmup:bathroom:frostProtectionMode"}
+```
+
+### Sitemap
+
+```
+Text label="Bathroom" {
+    Text item=bathroom_temperature
+    Setpoint item=bathroom_setpoint step=0.5
+    Text item=bathroom_overrideRemaining
+    Text item=bathroom_runMode
+    Switch item=bathroom_frostProtection
+}
+```
diff --git a/bundles/org.openhab.binding.warmup/pom.xml b/bundles/org.openhab.binding.warmup/pom.xml
new file mode 100644 (file)
index 0000000..d6905d5
--- /dev/null
@@ -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>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.warmup</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Warmup Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.warmup/src/main/feature/feature.xml b/bundles/org.openhab.binding.warmup/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..869399b
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.warmup-${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-warmup" description="Warmup Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.warmup/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/WarmupBindingConstants.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/WarmupBindingConstants.java
new file mode 100644 (file)
index 0000000..6b2d870
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link WarmupBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author James Melville - Initial contribution
+ */
+@NonNullByDefault
+public class WarmupBindingConstants {
+
+    private static final String BINDING_ID = "warmup";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "my-warmup");
+    public static final ThingTypeUID THING_TYPE_ROOM = new ThingTypeUID(BINDING_ID, "room");
+
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_ROOM);
+    public static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Set.of(THING_TYPE_ROOM);
+
+    // Room Channel Ids
+    public static final String CHANNEL_CURRENT_TEMPERATURE = "currentTemperature";
+    public static final String CHANNEL_TARGET_TEMPERATURE = "targetTemperature";
+    public static final String CHANNEL_OVERRIDE_DURATION = "overrideRemaining";
+    public static final String CHANNEL_RUN_MODE = "runMode";
+    public static final String CHANNEL_FROST_PROTECTION_MODE = "frostProtectionMode";
+    public static final String CHANNEL_HEATING_TARGET = "heatingTarget";
+    public static final String CHANNEL_AIR_TEMPERATURE = "airTemperature";
+    public static final String CHANNEL_FLOOR_TEMPERATURE = "floorTemperature";
+
+    public static final String FROST_PROTECTION_MODE = "anti_frost";
+
+    // Property Labels
+    public static final String PROPERTY_ROOM_ID = "Id";
+    public static final String PROPERTY_ROOM_NAME = "Name";
+    public static final String PROPERTY_LOCATION_ID = "LocationId";
+    public static final String PROPERTY_LOCATION_NAME = "Location";
+
+    // Web Service Endpoints
+    public static final String APP_ENDPOINT = "https://api.warmup.com/apps/app/v1";
+    public static final String QUERY_ENDPOINT = "https://apil.warmup.com/graphql";
+
+    // Web Service Constants
+    public static final String USER_AGENT = "WARMUP_APP";
+    public static final String APP_TOKEN = "M=;He<Xtg\"$}4N%5k{$:PD+WA\"]D<;#PriteY|VTuA>_iyhs+vA\"4lic{6-LqNM:";
+
+    public static final String AUTH_METHOD = "userLogin";
+    public static final String AUTH_APP_ID = "WARMUP-APP-V001";
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/api/MyWarmupApi.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/api/MyWarmupApi.java
new file mode 100644 (file)
index 0000000..fb39fcd
--- /dev/null
@@ -0,0 +1,190 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.api;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.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.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.warmup.internal.WarmupBindingConstants;
+import org.openhab.binding.warmup.internal.handler.MyWarmupConfigurationDTO;
+import org.openhab.binding.warmup.internal.model.auth.AuthRequestDTO;
+import org.openhab.binding.warmup.internal.model.auth.AuthResponseDTO;
+import org.openhab.binding.warmup.internal.model.query.QueryResponseDTO;
+import org.openhab.core.library.types.OnOffType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link MyWarmupApi} class contains code specific to calling the My Warmup API.
+ *
+ * @author James Melville - Initial contribution
+ */
+@NonNullByDefault
+public class MyWarmupApi {
+
+    private static final Gson GSON = new Gson();
+
+    private final Logger logger = LoggerFactory.getLogger(MyWarmupApi.class);
+    private final HttpClient httpClient;
+
+    private MyWarmupConfigurationDTO configuration;
+    private @Nullable String authToken;
+
+    /**
+     * Construct the API client
+     *
+     * @param httpClient HttpClient to make HTTP Calls
+     * @param configuration Thing configuration which contains API credentials
+     */
+    public MyWarmupApi(final HttpClient httpClient, MyWarmupConfigurationDTO configuration) {
+        this.httpClient = httpClient;
+        this.configuration = configuration;
+    }
+
+    /**
+     * Update the configuration, trigger a refresh of the access token
+     *
+     * @param configuration contains username and password
+     */
+    public void setConfiguration(MyWarmupConfigurationDTO configuration) {
+        authToken = null;
+        this.configuration = configuration;
+    }
+
+    private void validateSession() throws MyWarmupApiException {
+        if (authToken == null) {
+            authenticate();
+        }
+    }
+
+    private void authenticate() throws MyWarmupApiException {
+        String body = GSON.toJson(new AuthRequestDTO(configuration.username, configuration.password,
+                WarmupBindingConstants.AUTH_METHOD, WarmupBindingConstants.AUTH_APP_ID));
+
+        ContentResponse response = callWarmup(WarmupBindingConstants.APP_ENDPOINT, body, false);
+
+        AuthResponseDTO ar = GSON.fromJson(response.getContentAsString(), AuthResponseDTO.class);
+
+        if (ar != null && ar.getStatus() != null && ar.getStatus().getResult().equals("success")) {
+            authToken = ar.getResponse().getToken();
+        } else {
+            throw new MyWarmupApiException("Authentication Failed");
+        }
+    }
+
+    /**
+     * Query the API to get the status of all devices connected to the Bridge.
+     *
+     * @return The {@link QueryResponseDTO} object if retrieved, else null
+     * @throws MyWarmupApiException API callout error
+     */
+    public synchronized QueryResponseDTO getStatus() throws MyWarmupApiException {
+        return callWarmupGraphQL("query QUERY { user { locations{ id name "
+                + " rooms { id roomName runMode overrideDur targetTemp currentTemp "
+                + " thermostat4ies{ deviceSN lastPoll }}}}}");
+    }
+
+    /**
+     * Call the API to set a temperature override on a specific room
+     *
+     * @param locationId Id of the location
+     * @param roomId Id of the room
+     * @param temperature Temperature to set * 10
+     * @param duration Duration in minutes of the override
+     * @throws MyWarmupApiException API callout error
+     */
+    public void setOverride(String locationId, String roomId, int temperature, Integer duration)
+            throws MyWarmupApiException {
+        callWarmupGraphQL(String.format("mutation{deviceOverride(lid:%s,rid:%s,temperature:%d,minutes:%d)}", locationId,
+                roomId, temperature, duration));
+    }
+
+    /**
+     * Call the API to toggle frost protection mode on a specific room
+     *
+     * @param locationId Id of the location
+     * @param roomId Id of the room
+     * @param command Temperature to set
+     * @throws MyWarmupApiException API callout error
+     */
+    public void toggleFrostProtectionMode(String locationId, String roomId, OnOffType command)
+            throws MyWarmupApiException {
+        callWarmupGraphQL(String.format("mutation{turn%s(lid:%s,rid:%s){id}}", command == OnOffType.ON ? "Off" : "On",
+                locationId, roomId));
+    }
+
+    private QueryResponseDTO callWarmupGraphQL(String body) throws MyWarmupApiException {
+        validateSession();
+        ContentResponse response = callWarmup(WarmupBindingConstants.QUERY_ENDPOINT, "{\"query\": \"" + body + "\"}",
+                true);
+
+        QueryResponseDTO qr = GSON.fromJson(response.getContentAsString(), QueryResponseDTO.class);
+
+        if (qr != null && qr.getStatus().equals("success")) {
+            return qr;
+        } else {
+            throw new MyWarmupApiException("Unexpected reponse from API");
+        }
+    }
+
+    private synchronized ContentResponse callWarmup(String endpoint, String body, Boolean authenticated)
+            throws MyWarmupApiException {
+        try {
+            final Request request = httpClient.newRequest(endpoint);
+
+            request.method(HttpMethod.POST);
+
+            request.getHeaders().remove(HttpHeader.USER_AGENT);
+            request.header(HttpHeader.USER_AGENT, WarmupBindingConstants.USER_AGENT);
+            request.header(HttpHeader.CONTENT_TYPE, "application/json");
+            request.header("App-Token", WarmupBindingConstants.APP_TOKEN);
+            if (authenticated) {
+                request.header("Warmup-Authorization", authToken);
+            }
+
+            request.content(new StringContentProvider(body));
+
+            request.timeout(10, TimeUnit.SECONDS);
+
+            logger.trace("Sending body to My Warmup: Endpoint {}, Body {}", endpoint, body);
+            ContentResponse response = request.send();
+            logger.trace("Response from my warmup: Status {}, Body {}", response.getStatus(),
+                    response.getContentAsString());
+
+            if (response.getStatus() == HttpStatus.OK_200) {
+                return response;
+            } else if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
+                logger.debug("Authentication failure {} {}", response.getStatus(), response.getContentAsString());
+                authToken = null;
+                throw new MyWarmupApiException("Authentication failure");
+            } else {
+                logger.debug("Unexpected response {} {}", response.getStatus(), response.getContentAsString());
+            }
+            throw new MyWarmupApiException("Callout failed");
+        } catch (InterruptedException | TimeoutException | ExecutionException e) {
+            throw new MyWarmupApiException(e);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/api/MyWarmupApiException.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/api/MyWarmupApiException.java
new file mode 100644 (file)
index 0000000..ec468cb
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Exception thrown in case of api problems.
+ *
+ * @author James Melville - Initial contribution
+ */
+@SuppressWarnings("serial")
+@NonNullByDefault
+public class MyWarmupApiException extends Exception {
+
+    public MyWarmupApiException(@Nullable String message) {
+        super(message);
+    }
+
+    public MyWarmupApiException(@Nullable String message, @Nullable Throwable cause) {
+        super(message, cause);
+    }
+
+    public MyWarmupApiException(@Nullable Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/discovery/WarmupDiscoveryService.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/discovery/WarmupDiscoveryService.java
new file mode 100644 (file)
index 0000000..9439dd9
--- /dev/null
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.discovery;
+
+import static org.openhab.binding.warmup.internal.WarmupBindingConstants.*;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.warmup.internal.handler.MyWarmupAccountHandler;
+import org.openhab.binding.warmup.internal.handler.WarmupRefreshListener;
+import org.openhab.binding.warmup.internal.model.query.LocationDTO;
+import org.openhab.binding.warmup.internal.model.query.QueryResponseDTO;
+import org.openhab.binding.warmup.internal.model.query.RoomDTO;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+
+/**
+ * The {@link WarmupDiscoveryService} is used to discover devices that are connected to a My Warmup account.
+ *
+ * @author James Melville - Initial contribution
+ */
+@NonNullByDefault
+public class WarmupDiscoveryService extends AbstractDiscoveryService
+        implements DiscoveryService, ThingHandlerService, WarmupRefreshListener {
+
+    private @Nullable MyWarmupAccountHandler bridgeHandler;
+    private @Nullable ThingUID bridgeUID;
+
+    public WarmupDiscoveryService() {
+        super(DISCOVERABLE_THING_TYPES_UIDS, 5, false);
+    }
+
+    @Override
+    public void deactivate() {
+    }
+
+    @Override
+    public void startScan() {
+        final MyWarmupAccountHandler handler = bridgeHandler;
+        if (handler != null) {
+            removeOlderResults(getTimestampOfLastScan());
+            handler.setDiscoveryService(this);
+        }
+    }
+
+    /**
+     * Process device list and populate discovery list with things
+     *
+     * @param domain Data model representing all devices
+     */
+    @Override
+    public void refresh(@Nullable QueryResponseDTO domain) {
+        if (domain != null) {
+            HashSet<ThingUID> discoveredThings = new HashSet<ThingUID>();
+            for (LocationDTO location : domain.getData().getUser().getLocations()) {
+                for (RoomDTO room : location.getRooms()) {
+                    discoverRoom(location, room, discoveredThings);
+                }
+            }
+        }
+    }
+
+    private void discoverRoom(LocationDTO location, RoomDTO room, HashSet<ThingUID> discoveredThings) {
+        if (room.getThermostat4ies() != null && !room.getThermostat4ies().isEmpty()) {
+            final String deviceSN = room.getThermostat4ies().get(0).getDeviceSN();
+            ThingUID localBridgeUID = this.bridgeUID;
+            if (localBridgeUID != null && deviceSN != null) {
+                final Map<String, Object> roomProperties = new HashMap<>();
+                roomProperties.put(Thing.PROPERTY_SERIAL_NUMBER, deviceSN);
+                roomProperties.put(PROPERTY_ROOM_ID, room.getId());
+                roomProperties.put(PROPERTY_ROOM_NAME, room.getName());
+                roomProperties.put(PROPERTY_LOCATION_ID, location.getId());
+                roomProperties.put(PROPERTY_LOCATION_NAME, location.getName());
+
+                ThingUID roomThingUID = new ThingUID(THING_TYPE_ROOM, localBridgeUID, deviceSN);
+                thingDiscovered(DiscoveryResultBuilder.create(roomThingUID).withBridge(localBridgeUID)
+                        .withProperties(roomProperties).withLabel(location.getName() + " - " + room.getName())
+                        .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build());
+
+                discoveredThings.add(roomThingUID);
+            }
+        }
+    }
+
+    @Override
+    public void setThingHandler(@Nullable final ThingHandler handler) {
+        if (handler instanceof MyWarmupAccountHandler) {
+            bridgeHandler = (MyWarmupAccountHandler) handler;
+            bridgeUID = handler.getThing().getUID();
+        } else {
+            bridgeHandler = null;
+            bridgeUID = null;
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return bridgeHandler;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/MyWarmupAccountHandler.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/MyWarmupAccountHandler.java
new file mode 100644 (file)
index 0000000..c2d4ccf
--- /dev/null
@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.handler;
+
+import java.util.Collection;
+import java.util.Collections;
+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.warmup.internal.api.MyWarmupApi;
+import org.openhab.binding.warmup.internal.api.MyWarmupApiException;
+import org.openhab.binding.warmup.internal.discovery.WarmupDiscoveryService;
+import org.openhab.binding.warmup.internal.model.query.QueryResponseDTO;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+@NonNullByDefault
+public class MyWarmupAccountHandler extends BaseBridgeHandler {
+
+    private final MyWarmupApi api;
+
+    private @Nullable QueryResponseDTO queryResponse = null;
+
+    private @Nullable ScheduledFuture<?> refreshJob;
+    private @Nullable WarmupDiscoveryService discoveryService;
+
+    public MyWarmupAccountHandler(Bridge thing, final HttpClient httpClient) {
+        super(thing);
+        api = new MyWarmupApi(httpClient, getConfigAs(MyWarmupConfigurationDTO.class));
+    }
+
+    @Override
+    public void initialize() {
+        MyWarmupConfigurationDTO config = getConfigAs(MyWarmupConfigurationDTO.class);
+        if (config.username.length() == 0) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Username not configured");
+        } else if (config.password.length() == 0) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Password not configured");
+        } else if (config.refreshInterval >= 10) {
+            api.setConfiguration(config);
+            refreshJob = scheduler.scheduleWithFixedDelay(this::refreshFromServer, 0, config.refreshInterval,
+                    TimeUnit.SECONDS);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "Refresh interval misconfigured (minimum 10s)");
+        }
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(WarmupDiscoveryService.class);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+    }
+
+    @Override
+    public void dispose() {
+        cancelRefresh();
+    }
+
+    public void cancelRefresh() {
+        if (refreshJob != null) {
+            refreshJob.cancel(true);
+            refreshJob = null;
+        }
+    }
+
+    public synchronized void refreshFromServer() {
+        try {
+            queryResponse = api.getStatus();
+            updateStatus(ThingStatus.ONLINE);
+        } catch (MyWarmupApiException e) {
+            queryResponse = null;
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+        }
+        refreshFromCache();
+    }
+
+    /**
+     * Trigger updates to all devices
+     */
+    public synchronized void refreshFromCache() {
+        notifyListeners(queryResponse);
+    }
+
+    public void setDiscoveryService(final WarmupDiscoveryService discoveryService) {
+        this.discoveryService = discoveryService;
+        refreshFromServer();
+    }
+
+    public void unsetDiscoveryService() {
+        discoveryService = null;
+    }
+
+    /**
+     *
+     * @return reference to the bridge's API
+     */
+    public MyWarmupApi getApi() {
+        return api;
+    }
+
+    private void notifyListeners(@Nullable QueryResponseDTO domain) {
+        if (discoveryService != null && queryResponse != null) {
+            discoveryService.refresh(queryResponse);
+        }
+        getThing().getThings().stream().filter(thing -> thing.getHandler() instanceof WarmupRefreshListener)
+                .map(Thing::getHandler).map(WarmupRefreshListener.class::cast).forEach(thing -> thing.refresh(domain));
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/MyWarmupConfigurationDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/MyWarmupConfigurationDTO.java
new file mode 100644 (file)
index 0000000..945afee
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link MyWarmupConfigurationDTO} class contains fields mapping thing configuration parameters for the MyWarmup.
+ *
+ * @author James Melville - Initial contribution
+ */
+@NonNullByDefault
+public class MyWarmupConfigurationDTO {
+
+    public String username = "";
+    public String password = "";
+    public int refreshInterval = 300;
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/RoomConfigurationDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/RoomConfigurationDTO.java
new file mode 100644 (file)
index 0000000..6ceff99
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link RoomConfigurationDTO} class contains fields mapping thing configuration parameters for the Warmup Room.
+ *
+ * @author James Melville - Initial contribution
+ */
+@NonNullByDefault
+public class RoomConfigurationDTO {
+
+    private String serialNumber = "";
+    private int overrideDuration = 60;
+
+    public String getSerialNumber() {
+        return serialNumber;
+    }
+
+    public int getOverrideDuration() {
+        return overrideDuration;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/RoomHandler.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/RoomHandler.java
new file mode 100644 (file)
index 0000000..83dcb29
--- /dev/null
@@ -0,0 +1,151 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.handler;
+
+import static org.openhab.binding.warmup.internal.WarmupBindingConstants.*;
+
+import java.math.BigDecimal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.warmup.internal.api.MyWarmupApiException;
+import org.openhab.binding.warmup.internal.model.query.LocationDTO;
+import org.openhab.binding.warmup.internal.model.query.QueryResponseDTO;
+import org.openhab.binding.warmup.internal.model.query.RoomDTO;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.SIUnits;
+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.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+@NonNullByDefault
+public class RoomHandler extends WarmupThingHandler implements WarmupRefreshListener {
+
+    private final Logger logger = LoggerFactory.getLogger(RoomHandler.class);
+    private @Nullable RoomConfigurationDTO config;
+
+    public RoomHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        config = getConfigAs(RoomConfigurationDTO.class);
+        if (config.getSerialNumber().length() == 0) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Serial Number not configured");
+        } else {
+            super.refreshFromServer();
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        super.handleCommand(channelUID, command);
+        if (CHANNEL_TARGET_TEMPERATURE.equals(channelUID.getId()) && command instanceof QuantityType<?>) {
+            setOverride((QuantityType<?>) command);
+        }
+        if (CHANNEL_FROST_PROTECTION_MODE.equals(channelUID.getId()) && command instanceof OnOffType) {
+            toggleFrostProtectionMode((OnOffType) command);
+        }
+    }
+
+    /**
+     * Process device list and populate room properties, status and state
+     *
+     * @param domain Data model representing all devices
+     */
+    @Override
+    public void refresh(@Nullable QueryResponseDTO domain) {
+        if (domain == null) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No data from bridge");
+        } else if (config != null) {
+            final String serialNumber = config.getSerialNumber();
+            for (LocationDTO location : domain.getData().getUser().getLocations()) {
+                for (RoomDTO room : location.getRooms()) {
+                    if (room.getThermostat4ies() != null && !room.getThermostat4ies().isEmpty()
+                            && room.getThermostat4ies().get(0).getDeviceSN().equals(serialNumber)) {
+                        if (room.getThermostat4ies().get(0).getLastPoll() > 10) {
+                            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                                    "Thermostat has not polled for 10 minutes");
+                        } else {
+                            updateStatus(ThingStatus.ONLINE);
+
+                            updateProperty(PROPERTY_ROOM_ID, room.getId());
+                            updateProperty(PROPERTY_ROOM_NAME, room.getName());
+                            updateProperty(PROPERTY_LOCATION_ID, location.getId());
+                            updateProperty(PROPERTY_LOCATION_NAME, location.getName());
+
+                            updateState(CHANNEL_CURRENT_TEMPERATURE, parseTemperature(room.getCurrentTemperature()));
+                            updateState(CHANNEL_TARGET_TEMPERATURE, parseTemperature(room.getTargetTemperature()));
+                            updateState(CHANNEL_OVERRIDE_DURATION, parseDuration(room.getOverrideDuration()));
+                            updateState(CHANNEL_RUN_MODE, parseString(room.getRunMode()));
+                            updateState(CHANNEL_FROST_PROTECTION_MODE,
+                                    OnOffType.from(room.getRunMode().equals(FROST_PROTECTION_MODE)));
+                        }
+                        return;
+                    }
+                }
+            }
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Room not found");
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Room not configured");
+        }
+    }
+
+    private void setOverride(final QuantityType<?> command) {
+        String roomId = getThing().getProperties().get(PROPERTY_ROOM_ID);
+        String locationId = getThing().getProperties().get(PROPERTY_LOCATION_ID);
+
+        QuantityType<?> temp = command.toUnit(SIUnits.CELSIUS);
+
+        if (temp != null) {
+            final int value = temp.multiply(BigDecimal.TEN).intValue();
+
+            try {
+                final MyWarmupAccountHandler bridgeHandler = getBridgeHandler();
+                if (bridgeHandler != null && config != null) {
+                    final int overrideDuration = config.getOverrideDuration();
+                    if (overrideDuration > 0 && locationId != null && roomId != null) {
+                        bridgeHandler.getApi().setOverride(locationId, roomId, value, overrideDuration);
+                        refreshFromServer();
+                    }
+                }
+            } catch (MyWarmupApiException e) {
+                logger.debug("Set Override failed: {}", e.getMessage());
+            }
+        }
+    }
+
+    private void toggleFrostProtectionMode(OnOffType command) {
+        String roomId = getThing().getProperties().get(PROPERTY_ROOM_ID);
+        String locationId = getThing().getProperties().get(PROPERTY_LOCATION_ID);
+        try {
+            final MyWarmupAccountHandler bridgeHandler = getBridgeHandler();
+            if (bridgeHandler != null && locationId != null && roomId != null) {
+                bridgeHandler.getApi().toggleFrostProtectionMode(locationId, roomId, command);
+                refreshFromServer();
+            }
+        } catch (MyWarmupApiException e) {
+            logger.debug("Toggle Frost Protection failed: {}", e.getMessage());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupHandlerFactory.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupHandlerFactory.java
new file mode 100644 (file)
index 0000000..530dc1c
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.handler;
+
+import static org.openhab.binding.warmup.internal.WarmupBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+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 WarmupHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author James Melville - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.warmup", service = ThingHandlerFactory.class)
+public class WarmupHandlerFactory extends BaseThingHandlerFactory {
+
+    private final HttpClient httpClient;
+
+    @Activate
+    public WarmupHandlerFactory(@Reference final HttpClientFactory factory) {
+        httpClient = factory.getCommonHttpClient();
+    }
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
+            return new MyWarmupAccountHandler((Bridge) thing, httpClient);
+        } else if (THING_TYPE_ROOM.equals(thingTypeUID)) {
+            return new RoomHandler(thing);
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupRefreshListener.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupRefreshListener.java
new file mode 100644 (file)
index 0000000..c96bf51
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.warmup.internal.model.query.QueryResponseDTO;
+
+/**
+ * The {@link WarmupRefreshListener} is an interface applied to Things related to the Bridge allowing updates to be
+ * processed easily.
+ *
+ * @author James Melville - Initial contribution
+ */
+@NonNullByDefault
+public interface WarmupRefreshListener {
+
+    void refresh(@Nullable QueryResponseDTO domain);
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupThingHandler.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupThingHandler.java
new file mode 100644 (file)
index 0000000..c3fc299
--- /dev/null
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+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.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.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link WarmupThingHandler} is a super class for Things related to the Bridge consolidating logic.
+ *
+ * @author James Melville - Initial contribution
+ */
+@NonNullByDefault
+public class WarmupThingHandler extends BaseThingHandler {
+
+    public WarmupThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        final MyWarmupAccountHandler bridgeHandler = getBridgeHandler();
+        if (bridgeHandler != null) {
+            updateStatus(ThingStatus.UNKNOWN);
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        final MyWarmupAccountHandler bridgeHandler = getBridgeHandler();
+
+        if (command instanceof RefreshType && bridgeHandler != null) {
+            bridgeHandler.refreshFromCache();
+        }
+    }
+
+    protected void refreshFromServer() {
+        final MyWarmupAccountHandler bridgeHandler = getBridgeHandler();
+
+        if (bridgeHandler != null) {
+            bridgeHandler.refreshFromServer();
+        }
+    }
+
+    protected @Nullable MyWarmupAccountHandler getBridgeHandler() {
+        final Bridge bridge = getBridge();
+
+        if (bridge == null) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+            return null;
+        } else {
+            return (MyWarmupAccountHandler) bridge.getHandler();
+        }
+    }
+
+    /**
+     *
+     * @param temperature value returned from the API as an Integer * 10. i.e. 215 = 21.5 degrees C
+     * @return the temperature as a {@link QuantityType}
+     */
+    protected State parseTemperature(@Nullable Integer temperature) {
+        return temperature != null ? new QuantityType<>(temperature / 10.0, SIUnits.CELSIUS) : UnDefType.UNDEF;
+    }
+
+    /**
+     *
+     * @param value a string to convert to {@link StringType}
+     * @return the string as a {@link StringType}
+     */
+    protected State parseString(@Nullable String value) {
+        return value != null ? new StringType(value) : UnDefType.UNDEF;
+    }
+
+    /**
+     *
+     * @param value an integer to convert to {@link QuantityType} in minutes
+     * @return the number of minutes as a {@link QuantityType}
+     */
+    protected State parseDuration(@Nullable Integer value) {
+        return value != null ? new QuantityType<>(value, Units.MINUTE) : UnDefType.UNDEF;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthRequestDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthRequestDTO.java
new file mode 100644 (file)
index 0000000..6c45e45
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.model.auth;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+@SuppressWarnings("unused")
+public class AuthRequestDTO {
+
+    private AuthRequestDataDTO request;
+
+    public AuthRequestDTO(String email, String password, String method, String appId) {
+        setRequest(new AuthRequestDataDTO(email, password, method, appId));
+    }
+
+    public void setRequest(AuthRequestDataDTO request) {
+        this.request = request;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthRequestDataDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthRequestDataDTO.java
new file mode 100644 (file)
index 0000000..50fb967
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.model.auth;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+@SuppressWarnings("unused")
+public class AuthRequestDataDTO {
+    private String email;
+    private String password;
+    private String method;
+    private String appId;
+
+    public AuthRequestDataDTO(String email, String password, String method, String appId) {
+        this.setEmail(email);
+        this.setPassword(password);
+        this.setMethod(method);
+        this.setAppId(appId);
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public void setMethod(String method) {
+        this.method = method;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseDTO.java
new file mode 100644 (file)
index 0000000..2321720
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.model.auth;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+public class AuthResponseDTO {
+
+    private AuthResponseStatusDTO status;
+    private AuthResponseDataDTO response;
+
+    public AuthResponseStatusDTO getStatus() {
+        return status;
+    }
+
+    public AuthResponseDataDTO getResponse() {
+        return response;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseDataDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseDataDTO.java
new file mode 100644 (file)
index 0000000..cc673af
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.model.auth;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+public class AuthResponseDataDTO {
+    private String method;
+    private String token;
+
+    public String getToken() {
+        return token;
+    }
+
+    public String getMethod() {
+        return method;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseStatusDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseStatusDTO.java
new file mode 100644 (file)
index 0000000..821f691
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.model.auth;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+public class AuthResponseStatusDTO {
+    private String result;
+
+    public String getResult() {
+        return result;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/DeviceDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/DeviceDTO.java
new file mode 100644 (file)
index 0000000..61e21f3
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.model.query;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+public class DeviceDTO {
+
+    private String deviceSN;
+    private int lastPoll;
+
+    public String getDeviceSN() {
+        return deviceSN;
+    }
+
+    public int getLastPoll() {
+        return lastPoll;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/LocationDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/LocationDTO.java
new file mode 100644 (file)
index 0000000..cbcb2c4
--- /dev/null
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.model.query;
+
+import java.util.List;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+public class LocationDTO {
+
+    private int id;
+    private String name;
+    private List<RoomDTO> rooms;
+
+    public String getId() {
+        return String.valueOf(id);
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public List<RoomDTO> getRooms() {
+        return rooms;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/QueryDataDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/QueryDataDTO.java
new file mode 100644 (file)
index 0000000..be936db
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.model.query;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+public class QueryDataDTO {
+
+    private UserDTO user;
+
+    public UserDTO getUser() {
+        return user;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/QueryResponseDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/QueryResponseDTO.java
new file mode 100644 (file)
index 0000000..8bc2c50
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.model.query;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+public class QueryResponseDTO {
+
+    private QueryDataDTO data;
+    private String status;
+
+    public QueryDataDTO getData() {
+        return data;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/RoomDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/RoomDTO.java
new file mode 100644 (file)
index 0000000..42b77ca
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.model.query;
+
+import java.util.List;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+public class RoomDTO {
+
+    private int id;
+    private String roomName;
+    private Integer currentTemp;
+    private Integer targetTemp;
+    private String runMode;
+    private Integer overrideDur;
+    private List<DeviceDTO> thermostat4ies;
+
+    public String getId() {
+        return String.valueOf(id);
+    }
+
+    public String getName() {
+        return roomName;
+    }
+
+    public Integer getCurrentTemperature() {
+        return currentTemp;
+    }
+
+    public Integer getTargetTemperature() {
+        return targetTemp;
+    }
+
+    public String getRunMode() {
+        return runMode;
+    }
+
+    public Integer getOverrideDuration() {
+        return overrideDur;
+    }
+
+    public List<DeviceDTO> getThermostat4ies() {
+        return thermostat4ies;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/UserDTO.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/UserDTO.java
new file mode 100644 (file)
index 0000000..a6e0800
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.model.query;
+
+import java.util.List;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+public class UserDTO {
+
+    private List<LocationDTO> locations;
+
+    public List<LocationDTO> getLocations() {
+        return locations;
+    }
+}
diff --git a/bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..931dd32
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="warmup" 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>Warmup Binding</name>
+       <description>This is the binding for a Warmup 4iE Thermostat primarily used for controlling underfloor heating.</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..12cdb0b
--- /dev/null
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="warmup"
+       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="my-warmup">
+               <label>My Warmup Account</label>
+               <description>Connection to the https://my.warmup.com site</description>
+               <category>WebService</category>
+               <config-description>
+                       <parameter name="username" type="text" required="true">
+                               <context>email</context>
+                               <label>Username</label>
+                               <description>Username for my.warmup.com</description>
+                       </parameter>
+                       <parameter name="password" type="text" required="true">
+                               <context>password</context>
+                               <label>Password</label>
+                               <description>Password for my.warmup.com</description>
+                       </parameter>
+                       <parameter name="refreshInterval" type="integer" unit="s" required="true" min="10">
+                               <label>Refresh Interval</label>
+                               <description>Interval in seconds between automatic refreshes</description>
+                               <default>300</default>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+       <thing-type id="room">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="my-warmup"/>
+               </supported-bridge-type-refs>
+
+               <label>Room</label>
+               <description>Warmup 4iE Device controlling a room</description>
+               <category>RadiatorControl</category>
+
+               <channels>
+                       <channel id="currentTemperature" typeId="currentTemperature"/>
+                       <channel id="targetTemperature" typeId="targetTemperature"/>
+                       <channel id="overrideRemaining" typeId="overrideRemaining"/>
+                       <channel id="runMode" typeId="runMode"/>
+                       <channel id="frostProtectionMode" typeId="frostProtectionMode"/>
+               </channels>
+
+               <representation-property>serialNumber</representation-property>
+
+               <config-description>
+                       <parameter name="serialNumber" type="text" required="true">
+                               <label>Serial Number</label>
+                       </parameter>
+                       <parameter name="overrideDuration" type="integer" unit="m" required="true">
+                               <label>Override Duration</label>
+                               <description>Duration in minutes of override when target temperature is changed</description>
+                               <default>60</default>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <channel-type id="currentTemperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Current Temperature</label>
+               <description>Current temperature in room, may be air or floor dependent on Heating Target</description>
+               <category>Temperature</category>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="targetTemperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Target Temperature</label>
+               <description>Target temperature currently set on device</description>
+               <category>Heating</category>
+               <state min="5" max="30" step="0.5" readOnly="false" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="overrideRemaining">
+               <item-type>Number:Time</item-type>
+               <label>Override Remaining</label>
+               <description>How long until the override deactivates</description>
+               <category>Time</category>
+               <state readOnly="true" pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="runMode">
+               <item-type>String</item-type>
+               <label>Run Mode</label>
+               <description>The heat regulation mode of the thermostat</description>
+               <state readOnly="true">
+                       <options>
+                               <option value="not_set">Not Set</option>
+                               <option value="off">Off</option>
+                               <option value="schedule">Schedule</option>
+                               <option value="override">Override</option>
+                               <option value="fixed">Fixed</option>
+                               <option value="anti_frost">Frost Protection</option>
+                               <option value="holiday">Holiday</option>
+                               <option value="fil_pilote">Fil Pilote</option>
+                               <option value="gradual">Gradual</option>
+                               <option value="relay">Relay</option>
+                               <option value="previous">Previous</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="frostProtectionMode">
+               <item-type>Switch</item-type>
+               <label>Frost Protection Mode</label>
+       </channel-type>
+</thing:thing-descriptions>
index 280ee809dbc347386316d6344f9dc5466e486dd3..dbc247649ef8f3e17ca670babece5a3fdb13f49e 100644 (file)
     <module>org.openhab.binding.vigicrues</module>
     <module>org.openhab.binding.vitotronic</module>
     <module>org.openhab.binding.volvooncall</module>
+    <module>org.openhab.binding.warmup</module>
     <module>org.openhab.binding.weathercompany</module>
     <module>org.openhab.binding.weatherunderground</module>
     <module>org.openhab.binding.webthing</module>