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