diff --git a/CODEOWNERS b/CODEOWNERS
index bd3f9741e..16a0fb25e 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -305,6 +305,7 @@
/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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index b163c68bd..57f14d56c 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1501,6 +1501,11 @@
org.openhab.binding.volvooncall
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.warmup
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.weathercompany
diff --git a/bundles/org.openhab.binding.warmup/NOTICE b/bundles/org.openhab.binding.warmup/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.warmup/README.md b/bundles/org.openhab.binding.warmup/README.md
new file mode 100644
index 000000000..bc3f01f76
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/README.md
@@ -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]" (GF_Bathroom, Temperature) ["Temperature"] {channel="warmup:room:MyWarmup:bathroom:currentTemperature"}
+Number:Temperature bathroom_setpoint "Set Point [%.1f °C]" (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
index 000000000..d6905d572
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.warmup
+
+ openHAB Add-ons :: Bundles :: Warmup Binding
+
+
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
index 000000000..869399b33
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.warmup/${project.version}
+
+
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
index 000000000..6b2d87028
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/WarmupBindingConstants.java
@@ -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 SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_ROOM);
+ public static final Set 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_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
index 000000000..fb39fcd10
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/api/MyWarmupApi.java
@@ -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
index 000000000..ec468cb93
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/api/MyWarmupApiException.java
@@ -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
index 000000000..9439dd9d9
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/discovery/WarmupDiscoveryService.java
@@ -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 discoveredThings = new HashSet();
+ for (LocationDTO location : domain.getData().getUser().getLocations()) {
+ for (RoomDTO room : location.getRooms()) {
+ discoverRoom(location, room, discoveredThings);
+ }
+ }
+ }
+ }
+
+ private void discoverRoom(LocationDTO location, RoomDTO room, HashSet 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 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
index 000000000..c2d4ccf53
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/MyWarmupAccountHandler.java
@@ -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> 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
index 000000000..945afee2a
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/MyWarmupConfigurationDTO.java
@@ -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
index 000000000..6ceff990c
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/RoomConfigurationDTO.java
@@ -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
index 000000000..83dcb29d7
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/RoomHandler.java
@@ -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
index 000000000..530dc1c91
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupHandlerFactory.java
@@ -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
index 000000000..c96bf51e5
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupRefreshListener.java
@@ -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
index 000000000..c3fc299c7
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupThingHandler.java
@@ -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
index 000000000..6c45e4529
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthRequestDTO.java
@@ -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
index 000000000..50fb96700
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthRequestDataDTO.java
@@ -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
index 000000000..23217203f
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseDTO.java
@@ -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
index 000000000..cc673afe8
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseDataDTO.java
@@ -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
index 000000000..821f69102
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseStatusDTO.java
@@ -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
index 000000000..61e21f392
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/DeviceDTO.java
@@ -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
index 000000000..cbcb2c47a
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/LocationDTO.java
@@ -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 rooms;
+
+ public String getId() {
+ return String.valueOf(id);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public List 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
index 000000000..be936dbb8
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/QueryDataDTO.java
@@ -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
index 000000000..8bc2c50dd
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/QueryResponseDTO.java
@@ -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
index 000000000..42b77ca40
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/RoomDTO.java
@@ -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 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 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
index 000000000..a6e08002f
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/UserDTO.java
@@ -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 locations;
+
+ public List 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
index 000000000..931dd3238
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ Warmup Binding
+ This is the binding for a Warmup 4iE Thermostat primarily used for controlling underfloor heating.
+
+
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
index 000000000..12cdb0b14
--- /dev/null
+++ b/bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,110 @@
+
+
+
+
+
+ Connection to the https://my.warmup.com site
+ WebService
+
+
+ email
+
+ Username for my.warmup.com
+
+
+ password
+
+ Password for my.warmup.com
+
+
+
+ Interval in seconds between automatic refreshes
+ 300
+
+
+
+
+
+
+
+
+
+
+ Warmup 4iE Device controlling a room
+ RadiatorControl
+
+
+
+
+
+
+
+
+
+ serialNumber
+
+
+
+
+
+
+
+ Duration in minutes of override when target temperature is changed
+ 60
+
+
+
+
+
+ Number:Temperature
+
+ Current temperature in room, may be air or floor dependent on Heating Target
+ Temperature
+
+
+
+
+ Number:Temperature
+
+ Target temperature currently set on device
+ Heating
+
+
+
+
+ Number:Time
+
+ How long until the override deactivates
+ Time
+
+
+
+
+ String
+
+ The heat regulation mode of the thermostat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Switch
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 280ee809d..dbc247649 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -337,6 +337,7 @@
org.openhab.binding.vigicrues
org.openhab.binding.vitotronic
org.openhab.binding.volvooncall
+ org.openhab.binding.warmup
org.openhab.binding.weathercompany
org.openhab.binding.weatherunderground
org.openhab.binding.webthing