]> git.basschouten.com Git - openhab-addons.git/commitdiff
[warmup] Fixes & Enhancements (#16387)
authorJames Melville <jamesmelville@gmail.com>
Fri, 6 Sep 2024 21:44:28 +0000 (22:44 +0100)
committerGitHub <noreply@github.com>
Fri, 6 Sep 2024 21:44:28 +0000 (23:44 +0200)
* Update docs for multiple device support

Signed-off-by: James Melville <jamesmelville@gmail.com>
22 files changed:
bundles/org.openhab.binding.warmup/README.md
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/WarmupBindingConstants.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/action/WarmupActions.java [new file with mode: 0644]
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/api/MyWarmupApi.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/discovery/WarmupDiscoveryService.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/RoomHandler.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/handler/WarmupThingHandler.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthRequestDTO.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthRequestDataDTO.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseDTO.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseDataDTO.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/auth/AuthResponseStatusDTO.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/DeviceDTO.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/LocationDTO.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/QueryDataDTO.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/QueryResponseDTO.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/RoomDTO.java
bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/model/query/UserDTO.java
bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/addon/addon.xml
bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/i18n/warmup.properties
bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/update/warmup.xml [new file with mode: 0644]

index 7f454d42e44583e61862c602b74d3524b7ab2aac..51e48fe22390f54f543dec5e56997601a4d1b303 100644 (file)
@@ -1,11 +1,30 @@
 # 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/>.
+This binding integrates [Warmup](https://www.warmup.co.uk) Wifi enabled Thermostats via the API at <https://my.warmup.com/>.
 
-Any Warmup 4iE device(s) must be registered at <https://my.warmup.com/> prior to usage.
+Devices known to work with the binding:
+
+* [Warmup 4iE](https://www.warmup.co.uk/thermostats/smart/4ie-underfloor-heating)
+* [Warmup Element](https://www.warmup.co.uk/thermostats/smart/element-wifi-thermostat)
+
+Device expected to work with the binding:
+
+* [Warmup 6iE](https://www.warmup.co.uk/thermostats/smart/6ie-underfloor-heating)
+
+Devices which might work with the binding:
+
+* Other similar looking devices marketed under different brands, mentioned in the API
+  * [Laticrete](https://laticrete.com/)
+  * [Rointe](https://rointe.com/)
+  * [Porcelanosa](https://www.porcelanosa.com/)
+  * Equus
+  * [Savant](https://www.savant.com/)
+
+Any Warmup device must be registered at <https://my.warmup.com/> prior to usage, or connected through the [MyHeating app](https://www.warmup.co.uk/thermostats/smart/myheating-app).
 
 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.
+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](https://apil.warmup.com/graphql).
+
 
 ## Supported Things
 
@@ -15,15 +34,15 @@ The Warmup binding supports the following thing types:
 |----------------|-------------------|----------------------------------------------------------------------------------------|
 | `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. |
+| Thing    | Label | Description                                                                                    |
+|----------|-------|------------------------------------------------------------------------------------------------|
+| `room`   | Room  | A room containing an individual Warmup 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.
+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
 
@@ -41,12 +60,12 @@ Once credentials are successfully added to the bridge, any rooms (devices) detec
 
 ### 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.
+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. Changing the target temperature results in a temporary override to that temperature, for the 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      |
+| overrideDuration | Integer | Duration in minutes of override when target temperature is changed | false    | 60      |
 
 ## Channels
 
@@ -55,12 +74,20 @@ Rooms are configured automatically with a Serial Number on discovery, or can be
 | 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      |
+| fixedTemperature    | Number:Temperature | Target temperature for fixed mode                                                                                                            | false     |
+| energyToday         | Number:Energy      | Today's current energy consumption.                                                                                                          | true      |
+| runMode             | String             | Current operating mode of the thermostat, options listed below                                                                               | false     |
 | frostProtectionMode | Switch             | Toggles between the "Frost Protection" run mode and the previously configured "active" run mode (known options are either Fixed or Schedule) | false     |
+| airTemperature      | Number:Temperature | Currently reported air temperature at the device                                                                                             | true      |
+| floor1Temperature   | Number:Temperature | Currently reported temperature from floor probe 1 on the device                                                                              | true      |
+| floor2Temperature   | Number:Temperature | Currently reported temperature from floor probe 2 on the device                                                                              | true      |
 
 ### 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.
+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. 
+Only the value `schedule` is writeable, this reverts the device to the program/schedule configured on the device. 
+The value `fixed` can be set by commanding the `fixedTemperature` channel. The value `override` can be set by commanding the `targetTemperature` channel. 
 
 | api value  | ui name          | description                                                                     |
 |------------|------------------|---------------------------------------------------------------------------------|
@@ -76,6 +103,40 @@ These run mode statuses are defined for the API. The descriptions are based on i
 | relay      | Relay            | Unknown                                                                         |
 | previous   | Previous         | Unknown                                                                         |
 
+## Rule Actions
+
+### setOverride(temperature, duration)
+
+Sets a temporary temperature override on the device
+
+ Parameters:
+
+| Name        | Type                      | Description                                                             |
+|-------------|---------------------------|-------------------------------------------------------------------------|
+| temperature | QuantityType<Temperature> | Override temperature. Must be between 5°C and 30°C                      |
+| duration    | QuantityType<Time>        | Duration of the override. Must be between 0 and 1440 minutes (24 hours) |
+
+Example:
+
+:::: tabs
+
+::: tab DSL
+
+```javascript
+getActions("warmup", "warmup:room:my_warmup:my_room").setOverride(18 | °C, 10 | min);
+```
+
+:::
+
+::: tab JavaScript
+
+```javascript
+actions.get("warmup", "warmup:room:my_warmup:my_room").setOverride(Quantity("18 °C"), Quantity("10 min"));
+```
+:::
+
+::::
+
 ## Full Example
 
 ### .things file
index 0f3d0bd12b44c6d12e45e648fb8d59f8527fd110..f1fc73b5b38c43625fb1f612d58934952af22ac2 100644 (file)
@@ -38,12 +38,15 @@ public class WarmupBindingConstants {
     // Room Channel Ids
     public static final String CHANNEL_CURRENT_TEMPERATURE = "currentTemperature";
     public static final String CHANNEL_TARGET_TEMPERATURE = "targetTemperature";
+    public static final String CHANNEL_FIXED_TEMPERATURE = "fixedTemperature";
+    public static final String CHANNEL_ENERGY = "energyToday";
     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 CHANNEL_FLOOR1_TEMPERATURE = "floor1Temperature";
+    public static final String CHANNEL_FLOOR2_TEMPERATURE = "floor2Temperature";
 
     public static final String FROST_PROTECTION_MODE = "anti_frost";
 
@@ -63,4 +66,8 @@ public class WarmupBindingConstants {
 
     public static final String AUTH_METHOD = "userLogin";
     public static final String AUTH_APP_ID = "WARMUP-APP-V001";
+
+    public enum RoomMode {
+        SCHEDULE
+    }
 }
diff --git a/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/action/WarmupActions.java b/bundles/org.openhab.binding.warmup/src/main/java/org/openhab/binding/warmup/internal/action/WarmupActions.java
new file mode 100644 (file)
index 0000000..3960205
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.warmup.internal.action;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.warmup.internal.handler.RoomHandler;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingActionsScope;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author James Melville - Initial contribution
+ */
+@ThingActionsScope(name = "warmup")
+@NonNullByDefault
+public class WarmupActions implements ThingActions {
+
+    private final Logger logger = LoggerFactory.getLogger(WarmupActions.class);
+
+    private @Nullable RoomHandler handler;
+
+    public WarmupActions() {
+        logger.debug("Warmup action service instantiated");
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof RoomHandler roomHandler) {
+            this.handler = roomHandler;
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return handler;
+    }
+
+    @RuleAction(label = "override", description = "Overrides the thermostat state for a specified time")
+    public void setOverride(
+            @ActionInput(name = "temperature", label = "Temperature") @Nullable QuantityType<?> temperature,
+            @ActionInput(name = "duration", label = "Duration") @Nullable QuantityType<?> duration) {
+        logger.debug("setOverride action called");
+        RoomHandler handler = this.handler;
+        if (handler != null && temperature != null && duration != null) {
+            handler.setOverride(temperature, duration);
+        } else {
+            logger.warn("Warmup Action service argument is null!");
+        }
+    }
+
+    public static void setOverride(@Nullable ThingActions actions, @Nullable QuantityType<?> temperature,
+            @Nullable QuantityType<?> duration) {
+        if (actions instanceof WarmupActions warmupActions) {
+            warmupActions.setOverride(temperature, duration);
+        } else {
+            throw new IllegalArgumentException("Instance is not a WarmupActions class.");
+        }
+    }
+}
index a818add11f67a6957e5eb68c93796a20b7bcc894..095ac9efdd88bfc705dccf3bf40512026f71ea52 100644 (file)
@@ -87,8 +87,8 @@ public class MyWarmupApi {
 
         AuthResponseDTO ar = GSON.fromJson(response.getContentAsString(), AuthResponseDTO.class);
 
-        if (ar != null && ar.getStatus() != null && "success".equals(ar.getStatus().getResult())) {
-            authToken = ar.getResponse().getToken();
+        if (ar != null && ar.status() != null && "success".equals(ar.status().result())) {
+            authToken = ar.response().token();
         } else {
             throw new MyWarmupApiException("Authentication Failed");
         }
@@ -103,11 +103,39 @@ public class MyWarmupApi {
     public synchronized QueryResponseDTO getStatus() throws MyWarmupApiException {
         return callWarmupGraphQL("""
                 query QUERY { user { locations{ id name \
-                 rooms { id roomName runMode overrideDur targetTemp currentTemp \
-                 thermostat4ies{ deviceSN lastPoll }}}}}\
+                 rooms { id roomName energy runMode overrideDur targetTemp currentTemp fixedTemp \
+                 thermostat4ies{ deviceSN lastPoll airTemp floor1Temp floor2Temp }}}}}\
                 """);
     }
 
+    /**
+     * Call the API to set the room mode to program
+     *
+     * @param locationId Id of the location
+     * @param roomId Id of the room
+     * @param mode RoomMode defined in enum
+     * @throws MyWarmupApiException API callout error
+     */
+    public void setRoomMode(String locationId, String roomId, WarmupBindingConstants.RoomMode mode)
+            throws MyWarmupApiException {
+        if (WarmupBindingConstants.RoomMode.SCHEDULE.equals(mode)) {
+            callWarmupGraphQL("mutation{deviceProgram(lid:%s,rid:%s)}".formatted(locationId, roomId));
+        }
+    }
+
+    /**
+     * Call the API to set the room mode to fixed with a specific temperature
+     *
+     * @param locationId Id of the location
+     * @param roomId Id of the room
+     * @param temperature Temperature to set * 10
+     * @throws MyWarmupApiException API callout error
+     */
+    public void setFixed(String locationId, String roomId, int temperature) throws MyWarmupApiException {
+        callWarmupGraphQL(
+                "mutation{deviceFixed(lid:%s,rid:%s,temperature:%d)}".formatted(locationId, roomId, temperature));
+    }
+
     /**
      * Call the API to set a temperature override on a specific room
      *
@@ -119,7 +147,7 @@ public class MyWarmupApi {
      */
     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,
+        callWarmupGraphQL("mutation{deviceOverride(lid:%s,rid:%s,temperature:%d,minutes:%d)}".formatted(locationId,
                 roomId, temperature, duration));
     }
 
@@ -133,7 +161,7 @@ public class MyWarmupApi {
      */
     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",
+        callWarmupGraphQL("mutation{turn%s(lid:%s,rid:%s){id}}".formatted(command == OnOffType.ON ? "Off" : "On",
                 locationId, roomId));
     }
 
@@ -144,7 +172,7 @@ public class MyWarmupApi {
 
         QueryResponseDTO qr = GSON.fromJson(response.getContentAsString(), QueryResponseDTO.class);
 
-        if (qr != null && "success".equals(qr.getStatus())) {
+        if (qr != null && "success".equals(qr.status())) {
             return qr;
         } else {
             throw new MyWarmupApiException("Unexpected reponse from API");
index f41a61526090cca29c2fe4914dd6bac7464c5d32..753c3de542866be5e51068ba3978494ea433be88 100644 (file)
@@ -67,8 +67,8 @@ public class WarmupDiscoveryService extends AbstractThingHandlerDiscoveryService
     public void refresh(@Nullable QueryResponseDTO domain) {
         if (domain != null) {
             HashSet<ThingUID> discoveredThings = new HashSet<>();
-            for (LocationDTO location : domain.getData().getUser().getLocations()) {
-                for (RoomDTO room : location.getRooms()) {
+            for (LocationDTO location : domain.data().user().locations()) {
+                for (RoomDTO room : location.rooms()) {
                     discoverRoom(location, room, discoveredThings);
                 }
             }
@@ -76,20 +76,20 @@ public class WarmupDiscoveryService extends AbstractThingHandlerDiscoveryService
     }
 
     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();
+        if (room.thermostat4ies() != null && !room.thermostat4ies().isEmpty()) {
+            final String deviceSN = room.thermostat4ies().get(0).deviceSN();
             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_ROOM_NAME, room.roomName());
                 roomProperties.put(PROPERTY_LOCATION_ID, location.getId());
-                roomProperties.put(PROPERTY_LOCATION_NAME, location.getName());
+                roomProperties.put(PROPERTY_LOCATION_NAME, location.name());
 
                 ThingUID roomThingUID = new ThingUID(THING_TYPE_ROOM, localBridgeUID, deviceSN);
                 thingDiscovered(DiscoveryResultBuilder.create(roomThingUID).withBridge(localBridgeUID)
-                        .withProperties(roomProperties).withLabel(location.getName() + " - " + room.getName())
+                        .withProperties(roomProperties).withLabel(location.name() + " - " + room.roomName())
                         .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build());
 
                 discoveredThings.add(roomThingUID);
index 4170dc4add339b4f31de6d16d6e44625cb2f53e1..9840d8a72b2b63211d6c8ad3bcff6f62c0d2e62e 100644 (file)
@@ -14,21 +14,27 @@ package org.openhab.binding.warmup.internal.handler;
 
 import static org.openhab.binding.warmup.internal.WarmupBindingConstants.*;
 
-import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.warmup.internal.action.WarmupActions;
+import org.openhab.binding.warmup.internal.api.MyWarmupApi;
 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.library.types.StringType;
+import org.openhab.core.library.unit.Units;
 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.ThingHandlerService;
 import org.openhab.core.types.Command;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -64,9 +70,16 @@ public class RoomHandler extends WarmupThingHandler implements WarmupRefreshList
                 && command instanceof QuantityType<?> quantityCommand) {
             setOverride(quantityCommand);
         }
+        if (CHANNEL_FIXED_TEMPERATURE.equals(channelUID.getId())
+                && command instanceof QuantityType<?> quantityCommand) {
+            setFixed(quantityCommand);
+        }
         if (CHANNEL_FROST_PROTECTION_MODE.equals(channelUID.getId()) && command instanceof OnOffType onOffCommand) {
             toggleFrostProtectionMode(onOffCommand);
         }
+        if (CHANNEL_RUN_MODE.equals(channelUID.getId()) && command instanceof StringType stringCommand) {
+            setRoomMode(stringCommand);
+        }
     }
 
     /**
@@ -80,27 +93,35 @@ public class RoomHandler extends WarmupThingHandler implements WarmupRefreshList
             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) {
+            for (LocationDTO location : domain.data().user().locations()) {
+                for (RoomDTO room : location.rooms()) {
+                    if (room.thermostat4ies() != null && !room.thermostat4ies().isEmpty()
+                            && room.thermostat4ies().get(0).deviceSN().equals(serialNumber)) {
+                        if (room.thermostat4ies().get(0).lastPoll() > 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_ROOM_NAME, room.roomName());
                             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()));
+                            updateProperty(PROPERTY_LOCATION_NAME, location.name());
+
+                            updateState(CHANNEL_CURRENT_TEMPERATURE, parseTemperature(room.currentTemp()));
+                            updateState(CHANNEL_TARGET_TEMPERATURE, parseTemperature(room.targetTemp()));
+                            updateState(CHANNEL_FIXED_TEMPERATURE, parseTemperature(room.fixedTemp()));
+                            updateState(CHANNEL_ENERGY, parseEnergy(room.energy()));
+                            updateState(CHANNEL_AIR_TEMPERATURE,
+                                    parseTemperature(room.thermostat4ies().get(0).airTemp()));
+                            updateState(CHANNEL_FLOOR1_TEMPERATURE,
+                                    parseTemperature(room.thermostat4ies().get(0).floor1Temp()));
+                            updateState(CHANNEL_FLOOR2_TEMPERATURE,
+                                    parseTemperature(room.thermostat4ies().get(0).floor2Temp()));
+                            updateState(CHANNEL_OVERRIDE_DURATION, parseDuration(room.overrideDur()));
+                            updateState(CHANNEL_RUN_MODE, parseString(room.runMode()));
                             updateState(CHANNEL_FROST_PROTECTION_MODE,
-                                    OnOffType.from(room.getRunMode().equals(FROST_PROTECTION_MODE)));
+                                    OnOffType.from(room.runMode().equals(FROST_PROTECTION_MODE)));
                         }
                         return;
                     }
@@ -112,41 +133,82 @@ public class RoomHandler extends WarmupThingHandler implements WarmupRefreshList
         }
     }
 
-    private void setOverride(final QuantityType<?> command) {
-        String roomId = getThing().getProperties().get(PROPERTY_ROOM_ID);
-        String locationId = getThing().getProperties().get(PROPERTY_LOCATION_ID);
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Set.of(WarmupActions.class);
+    }
 
-        QuantityType<?> temp = command.toUnit(SIUnits.CELSIUS);
+    private void setOverride(final QuantityType<?> command) {
+        setOverride(command, new QuantityType<>(config.getOverrideDuration(), Units.MINUTE));
+    }
 
-        if (temp != null) {
-            final int value = temp.multiply(BigDecimal.TEN).intValue();
+    public void setOverride(final QuantityType<?> temperature, final QuantityType<?> duration) {
+        setOverride(formatTemperature(temperature), duration.toUnit(Units.MINUTE).intValue());
+    }
 
+    private void setOverride(final int temperature, final int duration) {
+        if (duration > 1440 || duration <= 0) {
+            logger.warn("Set Override failed: duration must be between 0 and 1440 minutes");
+        }
+        if (temperature > 600 || temperature < 50) {
+            logger.warn("Set Override failed: temperature must be between 0.5 and 60 degrees C");
+        } else {
             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();
-                    }
-                }
+                RoomCallout rc = getCallout();
+                rc.api.setOverride(rc.locationId, rc.roomId, temperature, duration);
+                refreshFromServer();
             } catch (MyWarmupApiException e) {
-                logger.debug("Set Override failed: {}", e.getMessage());
+                logger.warn("Set Override failed: {}", e.getMessage());
             }
         }
     }
 
+    private void setFixed(final QuantityType<?> command) {
+        try {
+            RoomCallout rc = getCallout();
+            rc.api.setFixed(rc.locationId, rc.roomId, formatTemperature(command));
+            refreshFromServer();
+        } catch (MyWarmupApiException e) {
+            logger.warn("Set Fixed 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();
-            }
+            RoomCallout rc = getCallout();
+            rc.api.toggleFrostProtectionMode(rc.locationId, rc.roomId, command);
+            refreshFromServer();
         } catch (MyWarmupApiException e) {
-            logger.debug("Toggle Frost Protection failed: {}", e.getMessage());
+            logger.warn("Toggle Frost Protection failed: {}", e.getMessage());
         }
     }
+
+    private void setRoomMode(StringType command) {
+        try {
+            RoomCallout rc = getCallout();
+            RoomMode mode = RoomMode.valueOf(command.toString().trim().toUpperCase());
+            rc.api.setRoomMode(rc.locationId, rc.roomId, mode);
+            refreshFromServer();
+        } catch (MyWarmupApiException e) {
+            logger.warn("Set Room Mode failed: {}", e.getMessage());
+        } catch (IllegalArgumentException ex) {
+            logger.warn("Unable to set room mode: {}", command.toString());
+        }
+    }
+
+    private RoomCallout getCallout() throws MyWarmupApiException {
+        Map<String, String> props = getThing().getProperties();
+        String locationId = props.get(PROPERTY_LOCATION_ID);
+        String roomId = props.get(PROPERTY_ROOM_ID);
+        final MyWarmupAccountHandler bridgeHandler = getBridgeHandler();
+
+        if (bridgeHandler != null && locationId != null && roomId != null) {
+            return new RoomCallout(roomId, locationId, bridgeHandler.getApi());
+        } else {
+            throw new MyWarmupApiException("Misconfigured thing.");
+        }
+    }
+
+    record RoomCallout(String roomId, String locationId, MyWarmupApi api) {
+    }
 }
index 92bc7edfda58dc005121b1faf7e9ae54eb630c56..faafbb5bc966dc5d291636c9d2e4867d1df52340 100644 (file)
@@ -77,6 +77,19 @@ public class WarmupThingHandler extends BaseThingHandler {
         }
     }
 
+    /**
+     *
+     * @param temperature value returned from the API as a String * 10. i.e. "215" = 21.5 degrees C
+     * @return the temperature as a {@link QuantityType}
+     */
+    protected State parseTemperature(@Nullable String temperature) {
+        try {
+            return temperature != null ? parseTemperature(Integer.parseInt(temperature)) : UnDefType.UNDEF;
+        } catch (NumberFormatException e) {
+            return UnDefType.UNDEF;
+        }
+    }
+
     /**
      *
      * @param temperature value returned from the API as an Integer * 10. i.e. 215 = 21.5 degrees C
@@ -86,6 +99,28 @@ public class WarmupThingHandler extends BaseThingHandler {
         return temperature != null ? new QuantityType<>(temperature / 10.0, SIUnits.CELSIUS) : UnDefType.UNDEF;
     }
 
+    /**
+     *
+     * @param temperature {@link QuantityType} a temperature
+     * @return the temperature as an int in degrees C * 10. i.e. 21.5 degrees C = 215
+     */
+    protected int formatTemperature(QuantityType<?> temperature) {
+        return (int) (temperature.toUnit(SIUnits.CELSIUS).doubleValue() * 10);
+    }
+
+    /**
+     *
+     * @param enery value returned from the API as a string "10.5" = 10.5 kWh
+     * @return the energy as a {@link QuantityType}
+     */
+    protected State parseEnergy(@Nullable String energy) {
+        try {
+            return energy != null ? new QuantityType<>(Float.parseFloat(energy), Units.KILOWATT_HOUR) : UnDefType.UNDEF;
+        } catch (NumberFormatException e) {
+            return UnDefType.UNDEF;
+        }
+    }
+
     /**
      *
      * @param value a string to convert to {@link StringType}
index 9797439fbb9db5038ce84006a2597053a0d2d23e..67b188ad1e931205cdf7cf6860b07819d54ce349 100644 (file)
@@ -15,16 +15,9 @@ package org.openhab.binding.warmup.internal.model.auth;
 /**
  * @author James Melville - Initial contribution
  */
-@SuppressWarnings("unused")
-public class AuthRequestDTO {
-
-    private AuthRequestDataDTO request;
+public record AuthRequestDTO(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;
+        this(new AuthRequestDataDTO(email, password, method, appId));
     }
 }
index 93c548706ea6c080c92537dda361d89730833410..608c805041187ed8b7f7af97894dbc41a02fdf16 100644 (file)
@@ -15,33 +15,5 @@ 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;
-    }
+public record AuthRequestDataDTO(String email, String password, String method, String appId) {
 }
index 7cfdd748e5811b03b955c635428ed5a890e8a83c..4104a9aeff1f17b1a3053326ebda838072c736d3 100644 (file)
@@ -15,16 +15,5 @@ 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;
-    }
+public record AuthResponseDTO(AuthResponseStatusDTO status, AuthResponseDataDTO response) {
 }
index 00c68c20b704da934b4e5d0a88de1b3afdd2535b..f25ba16992f0d7be6b1d81e3d8ed36a5b2b56eaf 100644 (file)
@@ -15,15 +15,5 @@ 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;
-    }
+public record AuthResponseDataDTO(String method, String token) {
 }
index 15a652e348df8633c7e46c358db9068f76067153..9d60f03497e6addbc46819a150d43c895e629424 100644 (file)
@@ -15,10 +15,5 @@ package org.openhab.binding.warmup.internal.model.auth;
 /**
  * @author James Melville - Initial contribution
  */
-public class AuthResponseStatusDTO {
-    private String result;
-
-    public String getResult() {
-        return result;
-    }
+public record AuthResponseStatusDTO(String result) {
 }
index df629f5e7cbdbbe5ef1f7f3cdea5ee3ddfe06ecb..d16ac62beb8184999400630a22482adb63e9e8e3 100644 (file)
@@ -15,16 +15,5 @@ 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;
-    }
+public record DeviceDTO(String deviceSN, String airTemp, String floor1Temp, String floor2Temp, int lastPoll) {
 }
index 30c3a4e2641d2b2eb5ba66f782fd2d44f1a5287c..6026724ba72920afbfb4d726f5934c670c884f0d 100644 (file)
@@ -17,21 +17,11 @@ import java.util.List;
 /**
  * @author James Melville - Initial contribution
  */
-public class LocationDTO {
+public record LocationDTO
 
-    private int id;
-    private String name;
-    private List<RoomDTO> rooms;
+(int id, String name, List<RoomDTO> rooms) {
 
     public String getId() {
         return String.valueOf(id);
     }
-
-    public String getName() {
-        return name;
-    }
-
-    public List<RoomDTO> getRooms() {
-        return rooms;
-    }
 }
index 52bf29741549f28ad61ab25789bbdd43f2068017..683f4fa7a22998486c166e661b96e7094b2feb96 100644 (file)
@@ -15,11 +15,5 @@ package org.openhab.binding.warmup.internal.model.query;
 /**
  * @author James Melville - Initial contribution
  */
-public class QueryDataDTO {
-
-    private UserDTO user;
-
-    public UserDTO getUser() {
-        return user;
-    }
+public record QueryDataDTO(UserDTO user) {
 }
index 6440b4ca275c093c917fec89aba3a9a741199968..31a0d92dcad56e9267fab27aa739e0bd39b5cd05 100644 (file)
@@ -15,16 +15,7 @@ package org.openhab.binding.warmup.internal.model.query;
 /**
  * @author James Melville - Initial contribution
  */
-public class QueryResponseDTO {
+public record QueryResponseDTO
 
-    private QueryDataDTO data;
-    private String status;
-
-    public QueryDataDTO getData() {
-        return data;
-    }
-
-    public String getStatus() {
-        return status;
-    }
+(QueryDataDTO data, String status) {
 }
index 72b67e2da4404f1e14e9cfa02b38f43cbcdcc4d5..8ce25a645d286d051a630cfab79a53a05a368eb1 100644 (file)
@@ -17,41 +17,12 @@ import java.util.List;
 /**
  * @author James Melville - Initial contribution
  */
-public class RoomDTO {
+public record RoomDTO(
 
-    private int id;
-    private String roomName;
-    private Integer currentTemp;
-    private Integer targetTemp;
-    private String runMode;
-    private Integer overrideDur;
-    private List<DeviceDTO> thermostat4ies;
+        int id, String roomName, Integer currentTemp, Integer targetTemp, Integer fixedTemp, String energy,
+        String runMode, Integer overrideDur, 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;
-    }
 }
index 59edfe2443bedbe63dc77e52ba0e0891784437d2..5a2d375d33e6715f6cfef82212366388d5e2ce49 100644 (file)
@@ -17,11 +17,5 @@ import java.util.List;
 /**
  * @author James Melville - Initial contribution
  */
-public class UserDTO {
-
-    private List<LocationDTO> locations;
-
-    public List<LocationDTO> getLocations() {
-        return locations;
-    }
+public record UserDTO(List<LocationDTO> locations) {
 }
index ec2052bb6a0865701628d0606eab38abe36a1b70..6220c007402e74ad75973d193c0303f605ac6fe3 100644 (file)
@@ -5,7 +5,8 @@
 
        <type>binding</type>
        <name>Warmup Binding</name>
-       <description>This is the binding for a Warmup 4iE Thermostat primarily used for controlling underfloor heating.</description>
+       <description>This is the binding for Warmup WiFi connected Thermostats primarily used for controlling underfloor
+               heating.</description>
        <connection>cloud</connection>
 
 </addon:addon>
index 627203350b8d724c7ee79c6156978bfa60276bf7..8fbc64b97726b9425fd7c96c5510d0d9d8c0463a 100644 (file)
@@ -1,14 +1,14 @@
 # add-on
 
 addon.warmup.name = Warmup Binding
-addon.warmup.description = This is the binding for a Warmup 4iE Thermostat primarily used for controlling underfloor heating.
+addon.warmup.description = This is the binding for Warmup WiFi connected Thermostats primarily used for controlling underfloor heating.
 
 # thing types
 
 thing-type.warmup.my-warmup.label = My Warmup Account
 thing-type.warmup.my-warmup.description = Connection to the https://my.warmup.com site
 thing-type.warmup.room.label = Room
-thing-type.warmup.room.description = Warmup 4iE Device controlling a room
+thing-type.warmup.room.description = Warmup WiFi connected Thermostat(s) controlling a room
 
 # thing types config
 
@@ -24,8 +24,18 @@ thing-type.config.warmup.room.serialNumber.label = Serial Number
 
 # channel types
 
+channel-type.warmup.airTemperature.label = Air Temperature
+channel-type.warmup.airTemperature.description = Currently reported air temperature at the device
 channel-type.warmup.currentTemperature.label = Current Temperature
 channel-type.warmup.currentTemperature.description = Current temperature in room, may be air or floor dependent on Heating Target
+channel-type.warmup.energyToday.label = Energy Today
+channel-type.warmup.energyToday.label = Today's current energy consumption.
+channel-type.warmup.fixedTemperature.label = Fixed Temperature
+channel-type.warmup.fixedTemperature.description = Target temperature for fixed mode on device
+channel-type.warmup.floor1Temperature.label = Floor 1 Temperature
+channel-type.warmup.floor1Temperature.description = Currently reported temperature from floor probe 1 on the device
+channel-type.warmup.floor2Temperature.label = Floor 2 Temperature
+channel-type.warmup.floor2Temperature.description = Currently reported temperature from floor probe 2 on the device
 channel-type.warmup.frostProtectionMode.label = Frost Protection Mode
 channel-type.warmup.overrideRemaining.label = Override Remaining
 channel-type.warmup.overrideRemaining.description = How long until the override deactivates
index 12cdb0b143225ffa992cf5bed9e022b531ba2ef6..9df11f7a1663c796f05c7935668434602e59b707 100644 (file)
                </supported-bridge-type-refs>
 
                <label>Room</label>
-               <description>Warmup 4iE Device controlling a room</description>
+               <description>Warmup WiFi connected Thermostat(s) controlling a room</description>
                <category>RadiatorControl</category>
 
                <channels>
-                       <channel id="currentTemperature" typeId="currentTemperature"/>
+                       <channel id="currentTemperature" typeId="system.indoor-temperature"/>
                        <channel id="targetTemperature" typeId="targetTemperature"/>
+                       <channel id="fixedTemperature" typeId="fixedTemperature"/>
                        <channel id="overrideRemaining" typeId="overrideRemaining"/>
+                       <channel id="energyToday" typeId="system.electric-energy"/>
                        <channel id="runMode" typeId="runMode"/>
                        <channel id="frostProtectionMode" typeId="frostProtectionMode"/>
+                       <channel id="airTemperature" typeId="system.indoor-temperature"/>
+                       <channel id="floor1Temperature" typeId="system.indoor-temperature"/>
+                       <channel id="floor2Temperature" typeId="system.indoor-temperature"/>
                </channels>
 
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <representation-property>serialNumber</representation-property>
 
                <config-description>
                </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>
                <state min="5" max="30" step="0.5" readOnly="false" pattern="%.1f %unit%"/>
        </channel-type>
 
+       <channel-type id="fixedTemperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Fixed Temperature</label>
+               <description>Target temperature for fixed mode 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>
@@ -86,7 +95,7 @@
                <item-type>String</item-type>
                <label>Run Mode</label>
                <description>The heat regulation mode of the thermostat</description>
-               <state readOnly="true">
+               <state>
                        <options>
                                <option value="not_set">Not Set</option>
                                <option value="off">Off</option>
diff --git a/bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/update/warmup.xml b/bundles/org.openhab.binding.warmup/src/main/resources/OH-INF/update/warmup.xml
new file mode 100644 (file)
index 0000000..fecb9f4
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
+
+       <thing-type uid="warmup:room">
+               <instruction-set targetVersion="1">
+                       <add-channel id="fixedTemperature">
+                               <type>warmup:fixedTemperature</type>
+                               <label>Fixed Temperature</label>
+                       </add-channel>
+                       <add-channel id="energyToday">
+                               <type>system.electric-energy</type>
+                               <label>Energy Today</label>
+                       </add-channel>
+                       <add-channel id="airTemperature">
+                               <type>system.indoor-temperature</type>
+                               <label>Air Temperature</label>
+                       </add-channel>
+                       <add-channel id="floor1Temperature">
+                               <type>system.indoor-temperature</type>
+                               <label>Floor 1 Temperature</label>
+                       </add-channel>
+                       <add-channel id="floor2Temperature">
+                               <type>system.indoor-temperature</type>
+                               <label>Floor 2 Temperature</label>
+                       </add-channel>
+                       <update-channel id="currentTemperature">
+                               <type>system.indoor-temperature</type>
+                       </update-channel>
+               </instruction-set>
+       </thing-type>
+</update:update-descriptions>