[warmup] Fixes & Enhancements (#16387)

* Update docs for multiple device support

Signed-off-by: James Melville <jamesmelville@gmail.com>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
James Melville 2024-09-06 22:44:28 +01:00 committed by Ciprian Pascu
parent f93ff0da03
commit 29d9e2effb
22 changed files with 414 additions and 225 deletions

View 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

View 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
}
}

View File

@ -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.");
}
}
}

View 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");

View 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);

View 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());
updateProperty(PROPERTY_LOCATION_NAME, location.name());
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_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
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(WarmupActions.class);
}
private void setOverride(final QuantityType<?> command) {
String roomId = getThing().getProperties().get(PROPERTY_ROOM_ID);
String locationId = getThing().getProperties().get(PROPERTY_LOCATION_ID);
setOverride(command, new QuantityType<>(config.getOverrideDuration(), Units.MINUTE));
}
QuantityType<?> temp = command.toUnit(SIUnits.CELSIUS);
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) {
}
}

View 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}

View 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));
}
}

View 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) {
}

View 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) {
}

View 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) {
}

View 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) {
}

View 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) {
}

View 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;
}
}

View 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) {
}

View 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) {
}

View 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;
}
}

View 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) {
}

View 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>

View 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

View File

@ -33,17 +33,26 @@
</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>
@ -58,14 +67,6 @@
</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>
@ -74,6 +75,14 @@
<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>

View File

@ -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>