[warmup] Initial contribution (#8562)

Signed-off-by: James Melville <jamesmelville@gmail.com>
This commit is contained in:
James Melville
2021-05-13 14:37:05 +01:00
committed by GitHub
parent 18497a9436
commit db05079e6f
31 changed files with 1603 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.warmup-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-warmup" description="Warmup Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.warmup/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.warmup.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link WarmupBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author James Melville - Initial contribution
*/
@NonNullByDefault
public class WarmupBindingConstants {
private static final String BINDING_ID = "warmup";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "my-warmup");
public static final ThingTypeUID THING_TYPE_ROOM = new ThingTypeUID(BINDING_ID, "room");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_ROOM);
public static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Set.of(THING_TYPE_ROOM);
// Room Channel Ids
public static final String CHANNEL_CURRENT_TEMPERATURE = "currentTemperature";
public static final String CHANNEL_TARGET_TEMPERATURE = "targetTemperature";
public static final String CHANNEL_OVERRIDE_DURATION = "overrideRemaining";
public static final String CHANNEL_RUN_MODE = "runMode";
public static final String CHANNEL_FROST_PROTECTION_MODE = "frostProtectionMode";
public static final String CHANNEL_HEATING_TARGET = "heatingTarget";
public static final String CHANNEL_AIR_TEMPERATURE = "airTemperature";
public static final String CHANNEL_FLOOR_TEMPERATURE = "floorTemperature";
public static final String FROST_PROTECTION_MODE = "anti_frost";
// Property Labels
public static final String PROPERTY_ROOM_ID = "Id";
public static final String PROPERTY_ROOM_NAME = "Name";
public static final String PROPERTY_LOCATION_ID = "LocationId";
public static final String PROPERTY_LOCATION_NAME = "Location";
// Web Service Endpoints
public static final String APP_ENDPOINT = "https://api.warmup.com/apps/app/v1";
public static final String QUERY_ENDPOINT = "https://apil.warmup.com/graphql";
// Web Service Constants
public static final String USER_AGENT = "WARMUP_APP";
public static final String APP_TOKEN = "M=;He<Xtg\"$}4N%5k{$:PD+WA\"]D<;#PriteY|VTuA>_iyhs+vA\"4lic{6-LqNM:";
public static final String AUTH_METHOD = "userLogin";
public static final String AUTH_APP_ID = "WARMUP-APP-V001";
}

View File

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

View File

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

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.warmup.internal.discovery;
import static org.openhab.binding.warmup.internal.WarmupBindingConstants.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.warmup.internal.handler.MyWarmupAccountHandler;
import org.openhab.binding.warmup.internal.handler.WarmupRefreshListener;
import org.openhab.binding.warmup.internal.model.query.LocationDTO;
import org.openhab.binding.warmup.internal.model.query.QueryResponseDTO;
import org.openhab.binding.warmup.internal.model.query.RoomDTO;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
/**
* The {@link WarmupDiscoveryService} is used to discover devices that are connected to a My Warmup account.
*
* @author James Melville - Initial contribution
*/
@NonNullByDefault
public class WarmupDiscoveryService extends AbstractDiscoveryService
implements DiscoveryService, ThingHandlerService, WarmupRefreshListener {
private @Nullable MyWarmupAccountHandler bridgeHandler;
private @Nullable ThingUID bridgeUID;
public WarmupDiscoveryService() {
super(DISCOVERABLE_THING_TYPES_UIDS, 5, false);
}
@Override
public void deactivate() {
}
@Override
public void startScan() {
final MyWarmupAccountHandler handler = bridgeHandler;
if (handler != null) {
removeOlderResults(getTimestampOfLastScan());
handler.setDiscoveryService(this);
}
}
/**
* Process device list and populate discovery list with things
*
* @param domain Data model representing all devices
*/
@Override
public void refresh(@Nullable QueryResponseDTO domain) {
if (domain != null) {
HashSet<ThingUID> discoveredThings = new HashSet<ThingUID>();
for (LocationDTO location : domain.getData().getUser().getLocations()) {
for (RoomDTO room : location.getRooms()) {
discoverRoom(location, room, discoveredThings);
}
}
}
}
private void discoverRoom(LocationDTO location, RoomDTO room, HashSet<ThingUID> discoveredThings) {
if (room.getThermostat4ies() != null && !room.getThermostat4ies().isEmpty()) {
final String deviceSN = room.getThermostat4ies().get(0).getDeviceSN();
ThingUID localBridgeUID = this.bridgeUID;
if (localBridgeUID != null && deviceSN != null) {
final Map<String, Object> roomProperties = new HashMap<>();
roomProperties.put(Thing.PROPERTY_SERIAL_NUMBER, deviceSN);
roomProperties.put(PROPERTY_ROOM_ID, room.getId());
roomProperties.put(PROPERTY_ROOM_NAME, room.getName());
roomProperties.put(PROPERTY_LOCATION_ID, location.getId());
roomProperties.put(PROPERTY_LOCATION_NAME, location.getName());
ThingUID roomThingUID = new ThingUID(THING_TYPE_ROOM, localBridgeUID, deviceSN);
thingDiscovered(DiscoveryResultBuilder.create(roomThingUID).withBridge(localBridgeUID)
.withProperties(roomProperties).withLabel(location.getName() + " - " + room.getName())
.withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build());
discoveredThings.add(roomThingUID);
}
}
}
@Override
public void setThingHandler(@Nullable final ThingHandler handler) {
if (handler instanceof MyWarmupAccountHandler) {
bridgeHandler = (MyWarmupAccountHandler) handler;
bridgeUID = handler.getThing().getUID();
} else {
bridgeHandler = null;
bridgeUID = null;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
}

View File

@@ -0,0 +1,134 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.warmup.internal.handler;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.warmup.internal.api.MyWarmupApi;
import org.openhab.binding.warmup.internal.api.MyWarmupApiException;
import org.openhab.binding.warmup.internal.discovery.WarmupDiscoveryService;
import org.openhab.binding.warmup.internal.model.query.QueryResponseDTO;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
/**
* @author James Melville - Initial contribution
*/
@NonNullByDefault
public class MyWarmupAccountHandler extends BaseBridgeHandler {
private final MyWarmupApi api;
private @Nullable QueryResponseDTO queryResponse = null;
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable WarmupDiscoveryService discoveryService;
public MyWarmupAccountHandler(Bridge thing, final HttpClient httpClient) {
super(thing);
api = new MyWarmupApi(httpClient, getConfigAs(MyWarmupConfigurationDTO.class));
}
@Override
public void initialize() {
MyWarmupConfigurationDTO config = getConfigAs(MyWarmupConfigurationDTO.class);
if (config.username.length() == 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Username not configured");
} else if (config.password.length() == 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Password not configured");
} else if (config.refreshInterval >= 10) {
api.setConfiguration(config);
refreshJob = scheduler.scheduleWithFixedDelay(this::refreshFromServer, 0, config.refreshInterval,
TimeUnit.SECONDS);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Refresh interval misconfigured (minimum 10s)");
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(WarmupDiscoveryService.class);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void dispose() {
cancelRefresh();
}
public void cancelRefresh() {
if (refreshJob != null) {
refreshJob.cancel(true);
refreshJob = null;
}
}
public synchronized void refreshFromServer() {
try {
queryResponse = api.getStatus();
updateStatus(ThingStatus.ONLINE);
} catch (MyWarmupApiException e) {
queryResponse = null;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
refreshFromCache();
}
/**
* Trigger updates to all devices
*/
public synchronized void refreshFromCache() {
notifyListeners(queryResponse);
}
public void setDiscoveryService(final WarmupDiscoveryService discoveryService) {
this.discoveryService = discoveryService;
refreshFromServer();
}
public void unsetDiscoveryService() {
discoveryService = null;
}
/**
*
* @return reference to the bridge's API
*/
public MyWarmupApi getApi() {
return api;
}
private void notifyListeners(@Nullable QueryResponseDTO domain) {
if (discoveryService != null && queryResponse != null) {
discoveryService.refresh(queryResponse);
}
getThing().getThings().stream().filter(thing -> thing.getHandler() instanceof WarmupRefreshListener)
.map(Thing::getHandler).map(WarmupRefreshListener.class::cast).forEach(thing -> thing.refresh(domain));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.warmup.internal.model.query;
import java.util.List;
/**
* @author James Melville - Initial contribution
*/
public class LocationDTO {
private int id;
private String name;
private List<RoomDTO> rooms;
public String getId() {
return String.valueOf(id);
}
public String getName() {
return name;
}
public List<RoomDTO> getRooms() {
return rooms;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.warmup.internal.model.query;
import java.util.List;
/**
* @author James Melville - Initial contribution
*/
public class RoomDTO {
private int id;
private String roomName;
private Integer currentTemp;
private Integer targetTemp;
private String runMode;
private Integer overrideDur;
private List<DeviceDTO> thermostat4ies;
public String getId() {
return String.valueOf(id);
}
public String getName() {
return roomName;
}
public Integer getCurrentTemperature() {
return currentTemp;
}
public Integer getTargetTemperature() {
return targetTemp;
}
public String getRunMode() {
return runMode;
}
public Integer getOverrideDuration() {
return overrideDur;
}
public List<DeviceDTO> getThermostat4ies() {
return thermostat4ies;
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.warmup.internal.model.query;
import java.util.List;
/**
* @author James Melville - Initial contribution
*/
public class UserDTO {
private List<LocationDTO> locations;
public List<LocationDTO> getLocations() {
return locations;
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="warmup" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Warmup Binding</name>
<description>This is the binding for a Warmup 4iE Thermostat primarily used for controlling underfloor heating.</description>
</binding:binding>

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="warmup"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="my-warmup">
<label>My Warmup Account</label>
<description>Connection to the https://my.warmup.com site</description>
<category>WebService</category>
<config-description>
<parameter name="username" type="text" required="true">
<context>email</context>
<label>Username</label>
<description>Username for my.warmup.com</description>
</parameter>
<parameter name="password" type="text" required="true">
<context>password</context>
<label>Password</label>
<description>Password for my.warmup.com</description>
</parameter>
<parameter name="refreshInterval" type="integer" unit="s" required="true" min="10">
<label>Refresh Interval</label>
<description>Interval in seconds between automatic refreshes</description>
<default>300</default>
</parameter>
</config-description>
</bridge-type>
<thing-type id="room">
<supported-bridge-type-refs>
<bridge-type-ref id="my-warmup"/>
</supported-bridge-type-refs>
<label>Room</label>
<description>Warmup 4iE Device controlling a room</description>
<category>RadiatorControl</category>
<channels>
<channel id="currentTemperature" typeId="currentTemperature"/>
<channel id="targetTemperature" typeId="targetTemperature"/>
<channel id="overrideRemaining" typeId="overrideRemaining"/>
<channel id="runMode" typeId="runMode"/>
<channel id="frostProtectionMode" typeId="frostProtectionMode"/>
</channels>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter name="serialNumber" type="text" required="true">
<label>Serial Number</label>
</parameter>
<parameter name="overrideDuration" type="integer" unit="m" required="true">
<label>Override Duration</label>
<description>Duration in minutes of override when target temperature is changed</description>
<default>60</default>
</parameter>
</config-description>
</thing-type>
<channel-type id="currentTemperature">
<item-type>Number:Temperature</item-type>
<label>Current Temperature</label>
<description>Current temperature in room, may be air or floor dependent on Heating Target</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="targetTemperature">
<item-type>Number:Temperature</item-type>
<label>Target Temperature</label>
<description>Target temperature currently set on device</description>
<category>Heating</category>
<state min="5" max="30" step="0.5" readOnly="false" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="overrideRemaining">
<item-type>Number:Time</item-type>
<label>Override Remaining</label>
<description>How long until the override deactivates</description>
<category>Time</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="runMode">
<item-type>String</item-type>
<label>Run Mode</label>
<description>The heat regulation mode of the thermostat</description>
<state readOnly="true">
<options>
<option value="not_set">Not Set</option>
<option value="off">Off</option>
<option value="schedule">Schedule</option>
<option value="override">Override</option>
<option value="fixed">Fixed</option>
<option value="anti_frost">Frost Protection</option>
<option value="holiday">Holiday</option>
<option value="fil_pilote">Fil Pilote</option>
<option value="gradual">Gradual</option>
<option value="relay">Relay</option>
<option value="previous">Previous</option>
</options>
</state>
</channel-type>
<channel-type id="frostProtectionMode">
<item-type>Switch</item-type>
<label>Frost Protection Mode</label>
</channel-type>
</thing:thing-descriptions>