added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.millheat</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

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

View File

@@ -0,0 +1,143 @@
# Millheat Binding
This binding integrates the Mill Wi-Fi enabled panel heaters.
See https://www.millheat.com/mill-wifi/
## Supported Things
This binding supports all Wi-Fi enabled heaters as well as the Wi-Fi socket.
* `account` = Mill Heating API - the account bridge
* `heater` = Panel/standalone heater
* `room` = A room defined in the mobile app
* `home` = A home defined in the mobile app
## Discovery
The binding will discover homes with rooms and heaters.
In order to do discovery, add a thing of type Mill Heating API and add username and password.
## Thing Configuration
See full example below for how to configure using thing files.
### Account
* `username` = email address used in app
* `password` = password used in app
* `refreshInterval` = number of seconds between refresh calls to the server
### Home
* `homeId` = id of home, type number (not string). Use auto discovery to find this value
### Room
* `roomId` = id of room, type number (not string). Use auto discovery to find this value
### Heater
* `macAddress` = network mac address of device.
Can be found in the app by viewing devices.
Or you can find it during discovery.
Used for heaters connected to a room.
* `heaterId` = id of device/heater, type number (not string)
Use auto discovery to find this value.
Used to identify independent heaters or heaters connected to a room.
* `power` = number of watts this heater is consuming when active.
Used to provide data for the currentPower channel.
Either `macAddres` or `heaterId` must be specified.
## Channels
### Home channels
| Channel | Read/write | Item type | Description |
| ------------------- | ------------- | ------------------- | ----------- |
| vacationMode | R/W | Switch | Vacation mode active. Note: In order to activate vacation mode, both vacationModeStart and vacationModeEnd must be set to valid values |
| vacationModeAdvanced | R/W | Switch | Vacation mode advanced active. Can only be activated after vacation mode is active |
| vacationModeTargetTemperature | R/W | Number:Temperature | Temperature to use when activating vacation mode. Note: If advanced vacation mode is set, this temperature is ignored and the away temperature for each room is used instead |
| vacationModeStart | R/W | DateTime | Vacation mode start |
| vacationModeEnd | R/W | DateTime | Vacation mode end |
### Room channels
| Channel | Read/write | Item type | Description |
| ------------------- | ------------- | --------------------- | ----------- |
| currentTemperature | R | Number:Temperature | Measured temperature in your room (if more than one heater then it is the average of all heaters) |
| currentMode | R | String | Current mode (comfort, away, sleep etc) being active |
| targetTemperature | R | Number:Temperature | Current target temperature for this room (managed by the room program and set by comfort- away- and sleepTemperature) |
| comfortTemperature | R/W | Number:Temperature | Comfort mode temperature |
| awayTemperature | R/W | Number:Temperature | Away mode temperature |
| sleepTemperature | R/W | Number:Temperature | Sleep mode temperature |
| heatingActive | R | Switch | Whether the heaters in this room are active |
| program | R | String | Name of program used in this room |
### Heater channels
| Channel | Read/write | Item type | Description |
| ------------------- | ------------- | ------------------ | ----------- |
| currentTemperature | R | Number:Temperature | Measured temperature by this heater |
| targetTemperature | R/W | Number:Temperature | Target temperature for this heater. Channel available only if heater is not connected to a room |
| currentPower | R | Number:Power | Current power usage in watts. Note that the power attribute of the heater thing config must be set for this channel to be active |
| heatingActive | R | Switch | Whether the heater is active/heating |
| fanActive | R/W | Switch | Whether the fan (if available) is active (UNTESTED) |
| independent | R | Switch | Whether this heater is controlled independently or part of a room setup |
| window | R | Contact | Whether this heater has detected that a window nearby is open/detection of cold air (UNTESTED) |
| masterSwitch | R/W | Switch | Turn heater ON/OFF. Channel available only if heater is not connected to a room |
## Full Example
millheat.things:
```
Bridge millheat:account:home "Millheat account" [username="email@address.com",password="topsecret"] {
Thing home monaco "Penthouse Monaco" [ homeId=100000000000000 ] // Note: numeric value
Thing room office "Office room" [ roomId=200000000000000 ] Note: numeric value
Thing heater office "Office panel heater" [ macAddress="F0XXXXXXXXX", power=900, heaterId=12345 ] Note: heaterId is a numeric value
}
```
millheat.items:
```
// Items connected to HOME channels
Number:Temperature Vacation_Target_Temperature "Vacation target temp [%d %unit%]" <temperature> {channel="millheat:home:home:monaco:vacationModeTargetTemperature"}
Switch Vacation_Mode "Vacation mode" <vacation> {channel="millheat:home:home:monaco:vacationMode"}
Switch Vacation_Mode_Advanced "Use room away temperatures" <vacation> {channel="millheat:home:home:monaco:vacationModeAdvanced"}
DateTime Vacation_Mode_Start "Vacation mode start [%1$td.%1$tm.%1$ty %1$tH:%1$tM]" <vacation> {channel="millheat:home:home:monaco:vacationModeStart"}
DateTime Vacation_Mode_End "Vacation mode end [%1$td.%1$tm.%1$ty %1$tH:%1$tM]" <vacation> {channel="millheat:home:home:monaco:vacationModeStart"}
// Items connected to ROOM channels
Number:Temperature Heating_Office_Room_Current_Temperature "Office current [%.1f %unit%]" <temperature> {channel="millheat:room:home:office:currentTemperature"}
Number:Temperature Heating_Office_Room_Target_Temperature "Office target [%.1f %unit%]" <temperature> {channel="millheat:room:home:office:targetTemperature"}
Number:Temperature Heating_Office_Room_Sleep_Temperature "Office sleep [%.1f %unit%]" <temperature> {channel="millheat:room:home:office:sleepTemperature"}
Number:Temperature Heating_Office_Room_Away_Temperature "Office away [%.1f %unit%]" <temperature> {channel="millheat:room:home:office:awayTemperature"}
Number:Temperature Heating_Office_Room_Comfort_Temperature "Office comfort [%.1f %unit%]" <temperature> {channel="millheat:room:home:office:comfortTemperature"}
Switch Heating_Office_Room_Heater_Active "Office active [%s]" <fire> {channel="millheat:room:home:office:heatingActive"}
String Heating_Office_Room_Mode "Office current mode [%s]" {channel="millheat:room:home:office:currentMode"}
String Heating_Office_Room_Program "Office program [%s]" {channel="millheat:room:home:office:program"}
// Items connected to HEATER channels
Number:Power Heating_Office_Heater_Current_Energy "Energy usage [%d W]" <energy> {channel="millheat:heater:home:office:currentEnergy"}
Number:Temperature Heating_Office_Heater_Current_Temperature "Heater current [%.1f %unit%]" <temperature> {channel="millheat:heater:home:office:currentTemperature"}
Number:Temperature Heating_Office_Heater_Target_Temperature "Heater target [%.1f %unit%]" <temperature> {channel="millheat:heater:home:office:targetTemperature"}
Switch Heating_Office_Heater_Heater_Active "Heater active [%s]" <fire> {channel="millheat:heater:home:office:heatingActive"}
Switch Heating_Office_Heater_Fan_Active "Fan active [%s]" <fan> {channel="millheat:heater:home:office:fanActive"}
Contact Heating_Office_Heater_Window "Window status [%s]" <window> {channel="millheat:heater:home:office:window"}
Switch Heating_Office_Heater_Independent "Heater independent [%s]" <switch> {channel="millheat:heater:home:office:independent"}
Switch Heating_Office_Heater_MasterSwitch "Heater masterswitch [%s]" <switch> {channel="millheat:heater:home:office:masterSwitch"}
```
## Setting up vacation mode
In order to activate vacation mode, follow these steps in a rule:
* Set start time (DateTime) on `DateTime` item linked to channel type `vacationModeStart`
* Set end time (DateTime) on `DateTime` item linked to channel type `vacationModeEnd`
* Activate vacation mode on `Switch` item linked to channel type `vacationMode`
* Optional - set advanced vacation mode on `Switch` item linked to channel type `vacationModeAdvanced`

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.millheat</artifactId>
<name>openHAB Add-ons :: Bundles :: Millheat Binding</name>
<dependencies>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>2.23.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.millheat-${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-millheat" description="Millheat Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.millheat/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link MillheatBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class MillheatBindingConstants {
private static final String BINDING_ID = "millheat";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_HOME = new ThingTypeUID(BINDING_ID, "home");
public static final ThingTypeUID THING_TYPE_ROOM = new ThingTypeUID(BINDING_ID, "room");
public static final ThingTypeUID THING_TYPE_HEATER = new ThingTypeUID(BINDING_ID, "heater");
// List of all Channel ids
public static final String CHANNEL_CURRENT_TEMPERATURE = "currentTemperature";
public static final String CHANNEL_COMFORT_TEMPERATURE = "comfortTemperature";
public static final String CHANNEL_SLEEP_TEMPERATURE = "sleepTemperature";
public static final String CHANNEL_AWAY_TEMPERATURE = "awayTemperature";
public static final String CHANNEL_HEATING_ACTIVE = "heatingActive";
public static final String CHANNEL_FAN_ACTIVE = "fanActive";
public static final String CHANNEL_TARGET_TEMPERATURE = "targetTemperature";
public static final String CHANNEL_CURRENT_POWER = "currentEnergy";
public static final String CHANNEL_CURRENT_MODE = "currentMode";
public static final String CHANNEL_PROGRAM = "program";
public static final String CHANNEL_INDEPENDENT = "independent";
public static final String CHANNEL_WINDOW_STATE = "window";
public static final String CHANNEL_MASTER_SWITCH = "masterSwitch";
// Vacation mode channels
public static final String CHANNEL_HOME_VACATION_TARGET_TEMPERATURE = "vacationModeTargetTemperature";
public static final String CHANNEL_HOME_VACATION_MODE = "vacationMode";
public static final String CHANNEL_HOME_VACATION_MODE_ADVANCED = "vacationModeAdvanced";
public static final String CHANNEL_HOME_VACATION_MODE_START = "vacationModeStart";
public static final String CHANNEL_HOME_VACATION_MODE_END = "vacationModeEnd";
public static final String CHANNEL_TYPE_MASTER_SWITCH = "masterSwitch";
public static final String CHANNEL_TYPE_TARGET_TEMPERATURE_HEATER = "targetTemperatureHeater";
public static final ChannelTypeUID CHANNEL_TYPE_MASTER_SWITCH_UID = new ChannelTypeUID(BINDING_ID,
CHANNEL_TYPE_MASTER_SWITCH);
public static final ChannelTypeUID CHANNEL_TYPE_TARGET_TEMPERATURE_HEATER_UID = new ChannelTypeUID(BINDING_ID,
CHANNEL_TYPE_TARGET_TEMPERATURE_HEATER);
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.millheat.internal.dto.AbstractRequest;
import org.openhab.binding.millheat.internal.dto.AbstractResponse;
/**
* The {@link MillheatCommunicationException} class wraps exceptions raised when communicating with the API
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class MillheatCommunicationException extends Exception {
private static final long serialVersionUID = 1L;
private int errorCode = 0;
public MillheatCommunicationException(final String message, final Throwable cause) {
super(message, cause);
}
public MillheatCommunicationException(final String message) {
super(message);
}
public MillheatCommunicationException(final AbstractRequest request, final AbstractResponse response) {
super("Server responded with error to request " + request.getClass().getSimpleName() + "/"
+ request.getRequestUrl() + ": " + response.errorCode + "/" + response.errorName + "/"
+ response.errorDescription);
this.errorCode = response.errorCode;
}
public int getErrorCode() {
return errorCode;
}
}

View File

@@ -0,0 +1,112 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.millheat.internal.discovery.MillheatDiscoveryService;
import org.openhab.binding.millheat.internal.handler.MillheatAccountHandler;
import org.openhab.binding.millheat.internal.handler.MillheatHeaterHandler;
import org.openhab.binding.millheat.internal.handler.MillheatHomeHandler;
import org.openhab.binding.millheat.internal.handler.MillheatRoomHandler;
import org.openhab.core.config.discovery.DiscoveryService;
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.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link MillheatHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.millheat", service = ThingHandlerFactory.class)
public class MillheatHandlerFactory extends BaseThingHandlerFactory {
private HttpClient httpClient;
private Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream
.of(MillheatBindingConstants.THING_TYPE_ACCOUNT, MillheatBindingConstants.THING_TYPE_HEATER,
MillheatBindingConstants.THING_TYPE_ROOM, MillheatBindingConstants.THING_TYPE_HOME)
.collect(Collectors.toSet()));
@Activate
public MillheatHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
protected @Nullable ThingHandler createHandler(final Thing thing) {
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (MillheatBindingConstants.THING_TYPE_HEATER.equals(thingTypeUID)) {
return new MillheatHeaterHandler(thing);
} else if (MillheatBindingConstants.THING_TYPE_ROOM.equals(thingTypeUID)) {
return new MillheatRoomHandler(thing);
} else if (MillheatBindingConstants.THING_TYPE_HOME.equals(thingTypeUID)) {
return new MillheatHomeHandler(thing);
} else if (MillheatBindingConstants.THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
final MillheatAccountHandler handler = new MillheatAccountHandler((Bridge) thing, httpClient,
bundleContext);
registerDeviceDiscoveryService(handler);
return handler;
}
return null;
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof MillheatAccountHandler) {
ThingUID thingUID = thingHandler.getThing().getUID();
unregisterDeviceDiscoveryService(thingUID);
}
super.removeHandler(thingHandler);
}
@Override
public boolean supportsThingType(final ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
private void registerDeviceDiscoveryService(MillheatAccountHandler bridgeHandler) {
MillheatDiscoveryService discoveryService = new MillheatDiscoveryService(bridgeHandler);
discoveryServiceRegs.put(bridgeHandler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
private void unregisterDeviceDiscoveryService(ThingUID thingUID) {
if (discoveryServiceRegs.containsKey(thingUID)) {
ServiceRegistration<?> serviceReg = discoveryServiceRegs.get(thingUID);
serviceReg.unregister();
discoveryServiceRegs.remove(thingUID);
}
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.client;
import java.lang.reflect.Type;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
/**
* The {@link BooleanSerializer} serializes and parses 1/0 to true/false from JSON files
*
* @author Arne Seime - Initial contribution
*/
public class BooleanSerializer implements JsonSerializer<Boolean>, JsonDeserializer<Boolean> {
@Override
public Boolean deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
return element.getAsInt() == 1;
}
@Override
public JsonElement serialize(final Boolean argument, final Type type, final JsonSerializationContext context) {
return new JsonPrimitive(Boolean.TRUE.equals(argument) ? 1 : 0);
}
}

View File

@@ -0,0 +1,134 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.client;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
/**
* Logs HttpClient request/response traffic.
*
* @author Gili Tzabari - Initial contribution https://stackoverflow.com/users/14731/gili
* https://stackoverflow.com/questions/50318736/how-to-log-httpclient-requests-response-including-body
* @author Arne Seime - adapted for Millheat binding
*/
@NonNullByDefault
public final class RequestLogger {
private final Logger logger = LoggerFactory.getLogger(RequestLogger.class);
private final AtomicLong nextId = new AtomicLong();
private final JsonParser parser;
private final Gson gson;
private final String prefix;
public RequestLogger(final String prefix, final Gson gson) {
this.parser = new JsonParser();
this.gson = gson;
this.prefix = prefix;
}
private void dump(final Request request) {
final long idV = nextId.getAndIncrement();
if (logger.isDebugEnabled()) {
final String id = prefix + "-" + idV;
final StringBuilder group = new StringBuilder();
request.onRequestBegin(theRequest -> group.append(
String.format("Request %s\n%s > %s %s\n", id, id, theRequest.getMethod(), theRequest.getURI())));
request.onRequestHeaders(theRequest -> {
for (final HttpField header : theRequest.getHeaders()) {
group.append(String.format("%s > %s\n", id, header));
}
});
final StringBuilder contentBuffer = new StringBuilder();
request.onRequestContent((theRequest, content) -> contentBuffer
.append(reformatJson(getCharset(theRequest.getHeaders()).decode(content).toString())));
request.onRequestSuccess(theRequest -> {
if (contentBuffer.length() > 0) {
group.append("\n");
group.append(contentBuffer);
}
String dataToLog = group.toString();
logger.debug(dataToLog);
contentBuffer.delete(0, contentBuffer.length());
group.delete(0, group.length());
});
request.onResponseBegin(theResponse -> {
group.append(String.format("Response %s\n%s < %s %s", id, id, theResponse.getVersion(),
theResponse.getStatus()));
if (theResponse.getReason() != null) {
group.append(" ");
group.append(theResponse.getReason());
}
group.append("\n");
});
request.onResponseHeaders(theResponse -> {
for (final HttpField header : theResponse.getHeaders()) {
group.append(String.format("%s < %s\n", id, header));
}
});
request.onResponseContent((theResponse, content) -> contentBuffer
.append(reformatJson(getCharset(theResponse.getHeaders()).decode(content).toString())));
request.onResponseSuccess(theResponse -> {
if (contentBuffer.length() > 0) {
group.append("\n");
group.append(contentBuffer);
}
String dataToLog = group.toString();
logger.debug(dataToLog);
});
}
}
private Charset getCharset(final HttpFields headers) {
final String contentType = headers.get(HttpHeader.CONTENT_TYPE);
if (contentType == null) {
return StandardCharsets.UTF_8;
}
final String[] tokens = contentType.toLowerCase(Locale.US).split("charset=");
if (tokens.length != 2) {
return StandardCharsets.UTF_8;
}
final String encoding = tokens[1].replaceAll("[;\"]", "");
return Charset.forName(encoding);
}
public Request listenTo(final Request request) {
dump(request);
return request;
}
private String reformatJson(final String jsonString) {
try {
final JsonElement json = parser.parse(jsonString);
return gson.toJson(json);
} catch (final JsonSyntaxException e) {
logger.debug("Could not reformat malformed JSON due to '{}'", e.getMessage());
return jsonString;
}
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.config;
/**
* The {@link MillheatAccountConfiguration} class contains account thing configuration parameters.
*
* @author Arne Seime - Initial contribution
*/
public class MillheatAccountConfiguration {
/**
* Username/email address used in app
*/
public String username;
public String password;
public int refreshInterval = 120;
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.config;
/**
* The {@link MillheatHeaterConfiguration} class contains heater thing configuration parameters.
*
* @author Arne Seime - Initial contribution
*/
public class MillheatHeaterConfiguration {
/*
* Wi-Fi mac address
*/
public String macAddress;
/*
* Wi-Fi heater id - found in logs
*/
public Long heaterId;
/*
* Nominal heater panel power
*/
public Integer power;
@Override
public String toString() {
return "MillheatHeaterConfiguration [macAddress=" + macAddress + ", heaterId=" + heaterId + ", power=" + power
+ "]";
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.config;
/**
* The {@link MillheatHomeConfiguration} class contains home thing configuration parameters.
*
* @author Arne Seime - Initial contribution
*/
public class MillheatHomeConfiguration {
public Long homeId;
@Override
public String toString() {
return "MillheatHomeConfiguration [homeId=" + homeId + "]";
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.config;
/**
* The {@link MillheatRoomConfiguration} class contains room thing configuration parameters.
*
* @author Arne Seime - Initial contribution
*/
public class MillheatRoomConfiguration {
/*
* Room ID
*/
public Long roomId;
@Override
public String toString() {
return "MillheatRoomConfiguration [roomId=" + roomId + "]";
}
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.discovery;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.openhab.binding.millheat.internal.MillheatBindingConstants;
import org.openhab.binding.millheat.internal.handler.MillheatAccountHandler;
import org.openhab.binding.millheat.internal.model.Heater;
import org.openhab.binding.millheat.internal.model.Home;
import org.openhab.binding.millheat.internal.model.MillheatModel;
import org.openhab.binding.millheat.internal.model.Room;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class does discovery of discoverable things
*
* @author Arne Seime - Initial contribution
*/
public class MillheatDiscoveryService extends AbstractDiscoveryService {
private static final long REFRESH_INTERVAL_MINUTES = 60;
public static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.of(MillheatBindingConstants.THING_TYPE_HEATER, MillheatBindingConstants.THING_TYPE_ROOM,
MillheatBindingConstants.THING_TYPE_HOME).collect(Collectors.toSet()));
private final Logger logger = LoggerFactory.getLogger(MillheatDiscoveryService.class);
private ScheduledFuture<?> discoveryJob;
private final MillheatAccountHandler accountHandler;
public MillheatDiscoveryService(final MillheatAccountHandler accountHandler) {
super(DISCOVERABLE_THING_TYPES_UIDS, 10);
this.accountHandler = accountHandler;
}
@Override
protected void startBackgroundDiscovery() {
discoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, REFRESH_INTERVAL_MINUTES, TimeUnit.MINUTES);
}
@Override
protected synchronized void startScan() {
try {
final ThingUID accountUID = accountHandler.getThing().getUID();
logger.debug("Start scan for Millheat devices on account {}", accountUID.toString());
accountHandler.updateModelFromServerWithRetry(false);
final MillheatModel model = accountHandler.getModel();
for (final Home home : model.getHomes()) {
final ThingUID homeUID = new ThingUID(MillheatBindingConstants.THING_TYPE_HOME, accountUID,
String.valueOf(home.getId()));
final DiscoveryResult discoveryResultHome = DiscoveryResultBuilder.create(homeUID)
.withBridge(accountUID).withLabel(home.getName()).withProperty("homeId", home.getId())
.withRepresentationProperty("homeId").build();
thingDiscovered(discoveryResultHome);
for (final Room room : home.getRooms()) {
final ThingUID roomUID = new ThingUID(MillheatBindingConstants.THING_TYPE_ROOM, accountUID,
String.valueOf(room.getId()));
final DiscoveryResult discoveryResultRoom = DiscoveryResultBuilder.create(roomUID)
.withBridge(accountUID).withLabel(room.getName()).withProperty("roomId", room.getId())
.withRepresentationProperty("roomId").build();
thingDiscovered(discoveryResultRoom);
for (final Heater heater : room.getHeaters()) {
final ThingUID heaterUID = new ThingUID(MillheatBindingConstants.THING_TYPE_HEATER, accountUID,
String.valueOf(heater.getId()));
final DiscoveryResult discoveryResultHeater = DiscoveryResultBuilder.create(heaterUID)
.withBridge(accountUID).withLabel(heater.getName())
.withProperty("heaterId", heater.getId()).withRepresentationProperty("macAddress")
.withProperty("macAddress", heater.getMacAddress()).build();
thingDiscovered(discoveryResultHeater);
}
}
for (final Heater heater : home.getIndependentHeaters()) {
final ThingUID heaterUID = new ThingUID(MillheatBindingConstants.THING_TYPE_HEATER, accountUID,
String.valueOf(heater.getId()));
final DiscoveryResult discoveryResultHeater = DiscoveryResultBuilder.create(heaterUID)
.withBridge(accountUID).withLabel(heater.getName()).withRepresentationProperty("heaterId")
.withProperty("heaterId", heater.getId()).build();
thingDiscovered(discoveryResultHeater);
}
}
} finally {
removeOlderResults(getTimestampOfLastScan(), null, accountHandler.getThing().getUID());
}
}
@Override
protected void stopBackgroundDiscovery() {
stopScan();
if (discoveryJob != null && !discoveryJob.isCancelled()) {
discoveryJob.cancel(true);
discoveryJob = null;
}
}
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
/**
* The {@link AbstractRequest} class is implemented by all service requests
**
* @author Arne Seime - Initial contribution
*/
public interface AbstractRequest {
String getRequestUrl();
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link AbstractResponse} class is the base class for all decoded JSON responses from the API
*
* @author Arne Seime - Initial contribution
*/
public abstract class AbstractResponse {
public static final int ERROR_CODE_ACCESS_TOKEN_EXPIRED = 3515;
public static final int ERROR_CODE_INVALID_SIGNATURE = 3015;
public static final int ERROR_CODE_AUTHENTICATION_FAILURE = 1025;
public int errorCode;
@SerializedName("error")
public String errorName;
@SerializedName("description")
public String errorDescription;
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link DeviceDTO} class represents a heater device
*
* @author Arne Seime - Initial contribution
*/
public class DeviceDTO {
public boolean heaterFlag;
public int subDomainId;
public int controlType;
public double currentTemp;
public boolean canChangeTemp;
public long deviceId;
public String deviceName;
@SerializedName("mac")
public String macAddress;
public int deviceStatus;
public int holidayTemp;
public boolean fanStatus;
@SerializedName("open")
public boolean openWindow;
public boolean powerStatus;
@SerializedName("isHoliday")
public boolean holiday;
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
/**
* This DTO class wraps the selectHomeList request
*
* @author Arne Seime - Initial contribution
*/
public class GetHomesRequest implements AbstractRequest {
@Override
public String getRequestUrl() {
return "selectHomeList";
}
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the selectHomeList response
*
* @author Arne Seime - Initial contribution
*/
public class GetHomesResponse extends AbstractResponse {
public Integer hourSystem;
@SerializedName("homeList")
public HomeDTO[] homes = new HomeDTO[0];
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
/**
* This DTO class wraps the get independent devices request
*
* @author Arne Seime - Initial contribution
*/
public class GetIndependentDevicesByHomeRequest implements AbstractRequest {
public final Long homeId;
public GetIndependentDevicesByHomeRequest(final Long homeId, final String timeZone) {
this.homeId = homeId;
}
@Override
public String getRequestUrl() {
return "getIndependentDevices";
}
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the get independent devices response
*
* @author Arne Seime - Initial contribution
*/
public class GetIndependentDevicesByHomeResponse extends AbstractResponse {
@SerializedName("deviceInfo")
public DeviceDTO devices[] = new DeviceDTO[0];
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the home dto json structure
*
* @author Arne Seime - Initial contribution
*/
public class HomeDTO {
public long homeId;
@SerializedName("homeAlways")
public boolean alwaysHome;
@SerializedName("homeName")
public String name;
@SerializedName("isHoliday")
public boolean holiday;
public Long holidayStartTime;
public String timeZone;
public Integer modeMinute;
public Long modeStartTime;
public Integer holidayTemp;
public Integer modeHour;
public Integer currentMode;
public Long holidayEndTime;
public Integer homeType;
public String programId;
public int holidayTempType;
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
/**
* This DTO class wraps the login request
*
* @author Arne Seime - Initial contribution
*/
public class LoginRequest implements AbstractRequest {
public final String account;
public final String password;
public LoginRequest(final String username, final String password) {
this.account = username;
this.password = password;
}
@Override
public String getRequestUrl() {
return "login";
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import java.util.Date;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the login response
*
* @author Arne Seime - Initial contribution
*/
public class LoginResponse extends AbstractResponse {
public String email;
@SerializedName("nickName")
public String nickname;
public String phone;
public String refreshToken;
public Date refreshTokenExpire;
public String token;
public Date tokenExpire;
public Integer userId;
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the room json structure
*
* @author Arne Seime - Initial contribution
*/
public class RoomDTO {
public long roomId;
@SerializedName("roomName")
public String name;
public int comfortTemp;
public int sleepTemp;
public int awayTemp;
@SerializedName("avgTemp")
public double currentTemp;
public String roomProgram;
public int currentMode = 0;
public boolean heatStatus = false;
@SerializedName("onLineDeviceNum")
public int onlineDeviceCount = 0;
@SerializedName("offLineDeviceNum")
public int offLineDeviceCount = 0;
@SerializedName("total")
public int totalCount = 0;
public int independentCount = 0;
@SerializedName("isOffline")
public boolean offline = true;
public String controlSource;
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the select device by room request
*
* @author Arne Seime - Initial contribution
*/
public class SelectDeviceByRoomRequest implements AbstractRequest {
public final Long roomId;
@SerializedName("timeZoneNum")
public final String timeZone;
public SelectDeviceByRoomRequest(final Long roomId, final String timeZone) {
this.roomId = roomId;
this.timeZone = timeZone;
}
@Override
public String getRequestUrl() {
return "selectDevicebyRoom";
}
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the select device by home response
*
* @author Arne Seime - Initial contribution
*/
public class SelectDeviceByRoomResponse extends AbstractResponse {
@SerializedName("deviceInfo")
public DeviceDTO[] devices = new DeviceDTO[0];
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the select room by home request
*
* @author Arne Seime - Initial contribution
*/
public class SelectRoomByHomeRequest implements AbstractRequest {
public final Long homeId;
@SerializedName("timeZoneNum")
public final String timeZone;
public SelectRoomByHomeRequest(final Long homeId, final String timeZone) {
this.homeId = homeId;
this.timeZone = timeZone;
}
@Override
public String getRequestUrl() {
return "selectRoombyHome";
}
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the select room by home response
*
* @author Arne Seime - Initial contribution
*/
public class SelectRoomByHomeResponse extends AbstractResponse {
@SerializedName("roomInfo")
public RoomDTO[] rooms = new RoomDTO[0];
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import org.openhab.binding.millheat.internal.model.Heater;
/**
* This DTO class wraps the set device temp request
*
* @see SetRoomTempRequest
* @author Arne Seime - Initial contribution
*/
public class SetDeviceTempRequest implements AbstractRequest {
public final int subDomain;
public final long deviceId;
public final boolean testStatus = true;
public final int operation;
public final boolean status;
public final boolean windStatus;
public final int holdTemp;
public final int tempType = 0; // FIXED?
public final int powerLevel = 0; // FIXED?
@Override
public String getRequestUrl() {
return "deviceControl";
}
public SetDeviceTempRequest(final Heater heater, final int targetTemperature, final boolean masterSwitch,
final boolean fanActive) {
this.subDomain = heater.getSubDomain();
this.deviceId = heater.getId();
this.holdTemp = targetTemperature;
this.status = masterSwitch;
this.windStatus = fanActive;
if (fanActive != heater.fanActive()) {
// Changed
operation = 4;
} else if (heater.getTargetTemp() != targetTemperature) {
operation = 1;
} else {
operation = 0;
}
}
}

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
/**
* This DTO class wraps the set device temp response
*
* @author Arne Seime - Initial contribution
*/
public class SetDeviceTempResponse extends AbstractResponse {
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* This DTO class wraps the set holiday parameter request
*
* @see HomeDTO
* @see GetHomesResponse
* @author Arne Seime - Initial contribution
*/
public class SetHolidayParameterRequest implements AbstractRequest {
public static final String PROP_TEMP = "holidayTemp";
public static final String PROP_MODE = "isHoliday";
public static final String PROP_MODE_ADVANCED = "holidayTempType";
public static final String PROP_START = "holidayStartTime";
public static final String PROP_END = "holidayEndTime";
// {"timeZoneNum":"-01:00","value":11,"homeList":[{"homeId":XXXXXXXXXXXX}],"key":"holidayTemp"}
public List<HomeID> homeList = new ArrayList<>();
@SerializedName("timeZoneNum")
public final String timeZone;
public final String key;
public final Object value;
/*
* Valid parameters: holidayTemp (degrees), holidayStartTime (secs since epoch), holidayEndTime (secs since epoch),
* isHoliday (boolean), holidayTempType (0 == advanced vacation mode - room uses it's own away temp, 1 == uses
* holidayTemp)
*/
public SetHolidayParameterRequest(Long homeId, String timeZone, String parameter, Object value) {
homeList.add(new HomeID(homeId));
this.timeZone = timeZone;
this.key = parameter;
this.value = value;
}
@Override
public String getRequestUrl() {
return "holidayChooseHome";
}
private class HomeID {
@SuppressWarnings("unused")
public Long homeId;
public HomeID(Long homeId) {
super();
this.homeId = homeId;
}
}
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
/**
* This DTO class wraps the set room temp response
*
* @author Arne Seime - Initial contribution
*/
public class SetHolidayParameterResponse extends AbstractResponse {
public String success;
public boolean isSuccess() {
return "true".contentEquals(success);
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
import org.openhab.binding.millheat.internal.model.Home;
import org.openhab.binding.millheat.internal.model.Room;
/**
* This DTO class wraps the set room temp request
*
* @see SetDeviceTempRequest
* @author Arne Seime - Initial contribution
*/
public class SetRoomTempRequest implements AbstractRequest {
public final long roomId;
public int comfortTemp;
public int sleepTemp;
public int awayTemp;
public final int homeType;
public SetRoomTempRequest(final Home home, final Room room) {
roomId = room.getId();
homeType = home.getType();
comfortTemp = room.getComfortTemp();
sleepTemp = room.getSleepTemp();
awayTemp = room.getAwayTemp();
}
@Override
public String getRequestUrl() {
return "changeRoomModeTempInfo";
}
}

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.dto;
/**
* This DTO class wraps the set room temp response
*
* @author Arne Seime - Initial contribution
*/
public class SetRoomTempResponse extends AbstractResponse {
}

View File

@@ -0,0 +1,493 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.handler;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
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.BytesContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.millheat.internal.MillheatCommunicationException;
import org.openhab.binding.millheat.internal.client.BooleanSerializer;
import org.openhab.binding.millheat.internal.client.RequestLogger;
import org.openhab.binding.millheat.internal.config.MillheatAccountConfiguration;
import org.openhab.binding.millheat.internal.dto.AbstractRequest;
import org.openhab.binding.millheat.internal.dto.AbstractResponse;
import org.openhab.binding.millheat.internal.dto.DeviceDTO;
import org.openhab.binding.millheat.internal.dto.GetHomesRequest;
import org.openhab.binding.millheat.internal.dto.GetHomesResponse;
import org.openhab.binding.millheat.internal.dto.GetIndependentDevicesByHomeRequest;
import org.openhab.binding.millheat.internal.dto.GetIndependentDevicesByHomeResponse;
import org.openhab.binding.millheat.internal.dto.HomeDTO;
import org.openhab.binding.millheat.internal.dto.LoginRequest;
import org.openhab.binding.millheat.internal.dto.LoginResponse;
import org.openhab.binding.millheat.internal.dto.RoomDTO;
import org.openhab.binding.millheat.internal.dto.SelectDeviceByRoomRequest;
import org.openhab.binding.millheat.internal.dto.SelectDeviceByRoomResponse;
import org.openhab.binding.millheat.internal.dto.SelectRoomByHomeRequest;
import org.openhab.binding.millheat.internal.dto.SelectRoomByHomeResponse;
import org.openhab.binding.millheat.internal.dto.SetDeviceTempRequest;
import org.openhab.binding.millheat.internal.dto.SetHolidayParameterRequest;
import org.openhab.binding.millheat.internal.dto.SetHolidayParameterResponse;
import org.openhab.binding.millheat.internal.dto.SetRoomTempRequest;
import org.openhab.binding.millheat.internal.dto.SetRoomTempResponse;
import org.openhab.binding.millheat.internal.model.Heater;
import org.openhab.binding.millheat.internal.model.Home;
import org.openhab.binding.millheat.internal.model.MillheatModel;
import org.openhab.binding.millheat.internal.model.ModeType;
import org.openhab.binding.millheat.internal.model.Room;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
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.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.util.HexUtils;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The {@link MillheatAccountHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class MillheatAccountHandler extends BaseBridgeHandler {
private static final String SHA_1_ALGORITHM = "SHA-1";
private static final int MIN_TIME_BETWEEEN_MODEL_UPDATES_MS = 30_000;
private static final int NUM_NONCE_CHARS = 16;
private static final String CONTENT_TYPE = "application/x-zc-object";
private static final String ALLOWED_NONCE_CHARACTERS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final int ALLOWED_NONCE_CHARACTERS_LENGTH = ALLOWED_NONCE_CHARACTERS.length();
private static final String REQUEST_TIMEOUT = "300";
public static String authEndpoint = "https://eurouter.ablecloud.cn:9005/zc-account/v1/";
public static String serviceEndpoint = "https://eurouter.ablecloud.cn:9005/millService/v1/";
private final Logger logger = LoggerFactory.getLogger(MillheatAccountHandler.class);
private @Nullable String userId;
private @Nullable String token;
private final HttpClient httpClient;
private final RequestLogger requestLogger;
private final Gson gson;
private MillheatModel model = new MillheatModel(0);
private @Nullable ScheduledFuture<?> statusFuture;
private @NonNullByDefault({}) MillheatAccountConfiguration config;
private static String getRandomString(final int sizeOfRandomString) {
final Random random = new Random();
final StringBuilder sb = new StringBuilder(sizeOfRandomString);
for (int i = 0; i < sizeOfRandomString; ++i) {
sb.append(ALLOWED_NONCE_CHARACTERS.charAt(random.nextInt(ALLOWED_NONCE_CHARACTERS_LENGTH)));
}
return sb.toString();
}
public MillheatAccountHandler(final Bridge bridge, final HttpClient httpClient, final BundleContext context) {
super(bridge);
this.httpClient = httpClient;
final BooleanSerializer serializer = new BooleanSerializer();
gson = new GsonBuilder().setPrettyPrinting().setDateFormat("yyyy-MM-dd HH:mm:ss")
.registerTypeAdapter(Boolean.class, serializer).registerTypeAdapter(boolean.class, serializer)
.setLenient().create();
requestLogger = new RequestLogger(bridge.getUID().getId(), gson);
}
private boolean allowModelUpdate() {
final long timeSinceLastUpdate = System.currentTimeMillis() - model.getLastUpdated();
if (timeSinceLastUpdate > MIN_TIME_BETWEEEN_MODEL_UPDATES_MS) {
return true;
}
return false;
}
public MillheatModel getModel() {
return model;
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
logger.debug("Bridge does not support any commands, but received command {} for channelUID {}", command,
channelUID);
}
public boolean doLogin() {
try {
final LoginResponse rsp = sendLoginRequest(new LoginRequest(config.username, config.password),
LoginResponse.class);
final int errorCode = rsp.errorCode;
if (errorCode != 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
String.format("Error login in: code=%s, type=%s, message=%s", errorCode, rsp.errorName,
rsp.errorDescription));
} else {
// No error provided on login, proceed to find token and userid
String localToken = rsp.token.trim();
userId = rsp.userId == null ? null : rsp.userId.toString();
if (localToken == null || localToken.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"error login in, no token provided");
} else if (userId == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"error login in, no userId provided");
} else {
token = localToken;
return true;
}
}
} catch (final MillheatCommunicationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error login: " + e.getMessage());
}
return false;
}
@Override
public void initialize() {
config = getConfigAs(MillheatAccountConfiguration.class);
scheduler.execute(() -> {
if (doLogin()) {
try {
model = refreshModel();
updateStatus(ThingStatus.ONLINE);
initPolling();
} catch (final MillheatCommunicationException e) {
model = new MillheatModel(0); // Empty model
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"error fetching initial data " + e.getMessage());
logger.debug("Error initializing Millheat data", e);
// Reschedule init
scheduler.schedule(() -> {
initialize();
}, 30, TimeUnit.SECONDS);
}
}
});
logger.debug("Finished initializing!");
}
@Override
public void dispose() {
stopPolling();
super.dispose();
}
/**
* starts this things polling future
*/
private void initPolling() {
stopPolling();
statusFuture = scheduler.scheduleWithFixedDelay(() -> {
try {
updateModelFromServerWithRetry(true);
} catch (final RuntimeException e) {
logger.debug("Error refreshing model", e);
}
}, config.refreshInterval, config.refreshInterval, TimeUnit.SECONDS);
}
private <T> T sendLoginRequest(final AbstractRequest req, final Class<T> responseType)
throws MillheatCommunicationException {
final Request request = httpClient.newRequest(authEndpoint + req.getRequestUrl());
addStandardHeadersAndPayload(request, req);
return sendRequest(request, req, responseType);
}
private <T> T sendLoggedInRequest(final AbstractRequest req, final Class<T> responseType)
throws MillheatCommunicationException {
try {
final Request request = buildLoggedInRequest(req);
return sendRequest(request, req, responseType);
} catch (NoSuchAlgorithmException e) {
throw new MillheatCommunicationException("Error building Millheat request: " + e.getMessage(), e);
}
}
@SuppressWarnings("unchecked")
private <T> T sendRequest(final Request request, final AbstractRequest req, final Class<T> responseType)
throws MillheatCommunicationException {
try {
final ContentResponse contentResponse = request.send();
final String responseJson = contentResponse.getContentAsString();
if (contentResponse.getStatus() == HttpStatus.OK_200) {
final AbstractResponse rsp = (AbstractResponse) gson.fromJson(responseJson, responseType);
if (rsp == null) {
return (T) null;
} else if (rsp.errorCode == 0) {
return (T) rsp;
} else {
throw new MillheatCommunicationException(req, rsp);
}
} else {
throw new MillheatCommunicationException(
"Error sending request to Millheat server. Server responded with " + contentResponse.getStatus()
+ " and payload " + responseJson);
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new MillheatCommunicationException("Error sending request to Millheat server: " + e.getMessage(), e);
}
}
public MillheatModel refreshModel() throws MillheatCommunicationException {
final MillheatModel model = new MillheatModel(System.currentTimeMillis());
final GetHomesResponse homesRsp = sendLoggedInRequest(new GetHomesRequest(), GetHomesResponse.class);
for (final HomeDTO dto : homesRsp.homes) {
model.addHome(new Home(dto));
}
for (final Home home : model.getHomes()) {
final SelectRoomByHomeResponse roomRsp = sendLoggedInRequest(
new SelectRoomByHomeRequest(home.getId(), home.getTimezone()), SelectRoomByHomeResponse.class);
for (final RoomDTO dto : roomRsp.rooms) {
home.addRoom(new Room(dto, home));
}
for (final Room room : home.getRooms()) {
final SelectDeviceByRoomResponse deviceRsp = sendLoggedInRequest(
new SelectDeviceByRoomRequest(room.getId(), home.getTimezone()),
SelectDeviceByRoomResponse.class);
for (final DeviceDTO dto : deviceRsp.devices) {
room.addHeater(new Heater(dto, room));
}
}
final GetIndependentDevicesByHomeResponse independentRsp = sendLoggedInRequest(
new GetIndependentDevicesByHomeRequest(home.getId(), home.getTimezone()),
GetIndependentDevicesByHomeResponse.class);
for (final DeviceDTO dto : independentRsp.devices) {
home.addHeater(new Heater(dto));
}
}
return model;
}
/**
* Stops this thing's polling future
*/
@SuppressWarnings("null")
private void stopPolling() {
if (statusFuture != null && !statusFuture.isCancelled()) {
statusFuture.cancel(true);
statusFuture = null;
}
}
public void updateModelFromServerWithRetry(boolean forceUpdate) {
if (allowModelUpdate() || forceUpdate) {
try {
updateModel();
} catch (final MillheatCommunicationException e) {
try {
if (AbstractResponse.ERROR_CODE_ACCESS_TOKEN_EXPIRED == e.getErrorCode()
|| AbstractResponse.ERROR_CODE_INVALID_SIGNATURE == e.getErrorCode()
|| AbstractResponse.ERROR_CODE_AUTHENTICATION_FAILURE == e.getErrorCode()) {
logger.debug("Token expired, will refresh token, then retry state refresh", e);
if (doLogin()) {
updateModel();
}
} else {
logger.debug("Initiating retry due to error {}", e.getMessage(), e);
updateModel();
}
} catch (MillheatCommunicationException e1) {
logger.debug("Retry failed, waiting for next refresh cycle: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.getMessage());
}
}
}
}
private void updateModel() throws MillheatCommunicationException {
model = refreshModel();
updateThingStatuses();
updateStatus(ThingStatus.ONLINE);
}
private void updateThingStatuses() {
final List<Thing> subThings = getThing().getThings();
for (final Thing thing : subThings) {
final ThingHandler handler = thing.getHandler();
if (handler != null) {
final MillheatBaseThingHandler mHandler = (MillheatBaseThingHandler) handler;
mHandler.updateState(model);
}
}
}
private Request buildLoggedInRequest(final AbstractRequest req) throws NoSuchAlgorithmException {
final String nonce = getRandomString(NUM_NONCE_CHARS);
final String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
final String signatureBasis = REQUEST_TIMEOUT + timestamp + nonce + token;
MessageDigest md = MessageDigest.getInstance(SHA_1_ALGORITHM);
byte[] sha1Hash = md.digest(signatureBasis.getBytes(StandardCharsets.UTF_8));
final String signature = HexUtils.bytesToHex(sha1Hash).toLowerCase();
final String reqJson = gson.toJson(req);
final Request request = httpClient.newRequest(serviceEndpoint + req.getRequestUrl());
return addStandardHeadersAndPayload(request, req).header("X-Zc-Timestamp", timestamp)
.header("X-Zc-Timeout", REQUEST_TIMEOUT).header("X-Zc-Nonce", nonce).header("X-Zc-User-Id", userId)
.header("X-Zc-User-Signature", signature).header("X-Zc-Content-Length", "" + reqJson.length());
}
private Request addStandardHeadersAndPayload(final Request req, final AbstractRequest payload) {
requestLogger.listenTo(req);
return req.header("Connection", "Keep-Alive").header("X-Zc-Major-Domain", "seanywell")
.header("X-Zc-Msg-Name", "millService").header("X-Zc-Sub-Domain", "milltype").header("X-Zc-Seq-Id", "1")
.header("X-Zc-Version", "1").method(HttpMethod.POST).timeout(5, TimeUnit.SECONDS)
.content(new BytesContentProvider(gson.toJson(payload).getBytes(StandardCharsets.UTF_8)), CONTENT_TYPE);
}
public void updateRoomTemperature(final Long roomId, final Command command, final ModeType mode) {
final Optional<Home> optionalHome = model.findHomeByRoomId(roomId);
final Optional<Room> optionalRoom = model.findRoomById(roomId);
if (optionalHome.isPresent() && optionalRoom.isPresent()) {
final SetRoomTempRequest req = new SetRoomTempRequest(optionalHome.get(), optionalRoom.get());
if (command instanceof QuantityType<?>) {
final int newTemp = (int) ((QuantityType<?>) command).longValue();
switch (mode) {
case SLEEP:
req.sleepTemp = newTemp;
break;
case AWAY:
req.awayTemp = newTemp;
break;
case COMFORT:
req.comfortTemp = newTemp;
break;
default:
logger.info("Cannot set room temp for mode {}", mode);
}
try {
sendLoggedInRequest(req, SetRoomTempResponse.class);
} catch (final MillheatCommunicationException e) {
logger.debug("Error updating temperature for room {}", roomId, e);
}
} else {
logger.debug("Error updating temperature for room {}, expected QuantityType but got {}", roomId,
command);
}
}
}
public void updateIndependentHeaterProperties(@Nullable final String macAddress, @Nullable final Long heaterId,
@Nullable final Command temperatureCommand, @Nullable final Command masterOnOffCommand,
@Nullable final Command fanCommand) {
model.findHeaterByMacOrId(macAddress, heaterId).ifPresent(heater -> {
int setTemp = heater.getTargetTemp();
if (temperatureCommand instanceof QuantityType<?>) {
setTemp = (int) ((QuantityType<?>) temperatureCommand).longValue();
}
boolean masterOnOff = heater.powerStatus();
if (masterOnOffCommand != null) {
masterOnOff = masterOnOffCommand == OnOffType.ON;
}
boolean fanActive = heater.fanActive();
if (fanCommand != null) {
fanActive = fanCommand == OnOffType.ON;
}
final SetDeviceTempRequest req = new SetDeviceTempRequest(heater, setTemp, masterOnOff, fanActive);
try {
sendLoggedInRequest(req, SetRoomTempResponse.class);
heater.setTargetTemp(setTemp);
heater.setPowerStatus(masterOnOff);
heater.setFanActive(fanActive);
} catch (final MillheatCommunicationException e) {
logger.debug("Error updating temperature for heater {}", macAddress, e);
}
});
}
public void updateVacationProperty(Home home, String property, Command command) {
try {
switch (property) {
case SetHolidayParameterRequest.PROP_START: {
long epoch = ((DateTimeType) command).getZonedDateTime().toEpochSecond();
SetHolidayParameterRequest req = new SetHolidayParameterRequest(home.getId(), home.getTimezone(),
SetHolidayParameterRequest.PROP_START, epoch);
if (sendLoggedInRequest(req, SetHolidayParameterResponse.class).isSuccess()) {
home.setVacationModeStart(epoch);
}
break;
}
case SetHolidayParameterRequest.PROP_END: {
long epoch = ((DateTimeType) command).getZonedDateTime().toEpochSecond();
SetHolidayParameterRequest req = new SetHolidayParameterRequest(home.getId(), home.getTimezone(),
SetHolidayParameterRequest.PROP_END, epoch);
if (sendLoggedInRequest(req, SetHolidayParameterResponse.class).isSuccess()) {
home.setVacationModeEnd(epoch);
}
break;
}
case SetHolidayParameterRequest.PROP_TEMP: {
int holidayTemp = ((QuantityType<?>) command).intValue();
SetHolidayParameterRequest req = new SetHolidayParameterRequest(home.getId(), home.getTimezone(),
SetHolidayParameterRequest.PROP_TEMP, holidayTemp);
if (sendLoggedInRequest(req, SetHolidayParameterResponse.class).isSuccess()) {
home.setHolidayTemp(holidayTemp);
}
break;
}
case SetHolidayParameterRequest.PROP_MODE_ADVANCED: {
if (home.getMode().getMode() == ModeType.VACATION) {
int value = OnOffType.ON == command ? 0 : 1;
SetHolidayParameterRequest req = new SetHolidayParameterRequest(home.getId(),
home.getTimezone(), SetHolidayParameterRequest.PROP_MODE_ADVANCED, value);
if (sendLoggedInRequest(req, SetHolidayParameterResponse.class).isSuccess()) {
home.setVacationModeAdvanced((OnOffType) command);
}
} else {
logger.debug("Must enable vaction mode before advanced vacation mode can be enabled");
}
break;
}
case SetHolidayParameterRequest.PROP_MODE: {
if (home.getVacationModeStart() != null && home.getVacationModeEnd() != null) {
int value = OnOffType.ON == command ? 1 : 0;
SetHolidayParameterRequest req = new SetHolidayParameterRequest(home.getId(),
home.getTimezone(), SetHolidayParameterRequest.PROP_MODE, value);
if (sendLoggedInRequest(req, SetHolidayParameterResponse.class).isSuccess()) {
updateModelFromServerWithRetry(true);
}
} else {
logger.debug("Cannot enable vacation mode unless start and end time is already set");
}
break;
}
}
} catch (MillheatCommunicationException e) {
logger.debug("Failure trying to set holiday properties: {}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.handler;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.millheat.internal.model.MillheatModel;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for heater and room handlers
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public abstract class MillheatBaseThingHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(MillheatBaseThingHandler.class);
public MillheatBaseThingHandler(final Thing thing) {
super(thing);
}
public void updateState(final MillheatModel model) {
for (final Channel channel : getThing().getChannels()) {
handleCommand(channel.getUID(), RefreshType.REFRESH, model);
}
}
protected MillheatModel getMillheatModel() {
final Optional<MillheatAccountHandler> accountHandler = getAccountHandler();
if (accountHandler.isPresent()) {
return accountHandler.get().getModel();
} else {
logger.warn(
"Thing {} cannot exist without a bridge and account handler - returning empty model. No heaters or rooms will be found",
getThing().getUID());
return new MillheatModel(0);
}
}
protected Optional<MillheatAccountHandler> getAccountHandler() {
final Bridge bridge = getBridge();
if (bridge != null) {
MillheatAccountHandler handler = (MillheatAccountHandler) bridge.getHandler();
if (handler != null) {
return Optional.of(handler);
}
}
return Optional.empty();
}
protected abstract void handleCommand(ChannelUID uid, Command command, MillheatModel model);
}

View File

@@ -0,0 +1,193 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.handler;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.millheat.internal.MillheatBindingConstants;
import org.openhab.binding.millheat.internal.config.MillheatHeaterConfiguration;
import org.openhab.binding.millheat.internal.model.Heater;
import org.openhab.binding.millheat.internal.model.MillheatModel;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.Channel;
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.builder.ChannelBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MillheatHeaterHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class MillheatHeaterHandler extends MillheatBaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(MillheatHeaterHandler.class);
private @NonNullByDefault({}) MillheatHeaterConfiguration config;
public MillheatHeaterHandler(final Thing thing) {
super(thing);
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
handleCommand(channelUID, command, getMillheatModel());
}
@Override
protected void handleCommand(final ChannelUID channelUID, final Command command, final MillheatModel model) {
final Optional<Heater> optionalHeater = model.findHeaterByMacOrId(config.macAddress, config.heaterId);
if (optionalHeater.isPresent()) {
updateStatus(ThingStatus.ONLINE);
final Heater heater = optionalHeater.get();
if (MillheatBindingConstants.CHANNEL_CURRENT_TEMPERATURE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, new QuantityType<>(heater.getCurrentTemp(), SIUnits.CELSIUS));
}
} else if (MillheatBindingConstants.CHANNEL_HEATING_ACTIVE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, heater.isHeatingActive() ? OnOffType.ON : OnOffType.OFF);
}
} else if (MillheatBindingConstants.CHANNEL_FAN_ACTIVE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, heater.fanActive() ? OnOffType.ON : OnOffType.OFF);
} else if (heater.canChangeTemp() && heater.getRoom() == null) {
updateIndependentHeaterProperties(null, null, command);
} else {
logger.debug("Heater {} cannot change temperature and is in a room", getThing().getUID());
}
} else if (MillheatBindingConstants.CHANNEL_WINDOW_STATE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, heater.windowOpen() ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
}
} else if (MillheatBindingConstants.CHANNEL_INDEPENDENT.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, heater.getRoom() == null ? OnOffType.ON : OnOffType.OFF);
}
} else if (MillheatBindingConstants.CHANNEL_CURRENT_POWER.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
if (config.power != null) {
if (heater.isHeatingActive()) {
updateState(channelUID, new QuantityType<>(config.power, SmartHomeUnits.WATT));
} else {
updateState(channelUID, new QuantityType<>(0, SmartHomeUnits.WATT));
}
} else {
updateState(channelUID, UnDefType.UNDEF);
logger.debug(
"Cannot update power for heater as the nominal power has not been configured for thing {}",
getThing().getUID());
}
}
} else if (MillheatBindingConstants.CHANNEL_TARGET_TEMPERATURE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
if (heater.canChangeTemp() && heater.getTargetTemp() != null) {
updateState(channelUID, new QuantityType<>(heater.getTargetTemp(), SIUnits.CELSIUS));
} else if (heater.getRoom() != null) {
final Integer targetTemperature = heater.getRoom().getTargetTemperature();
if (targetTemperature != null) {
updateState(channelUID, new QuantityType<>(targetTemperature, SIUnits.CELSIUS));
} else {
updateState(channelUID, UnDefType.UNDEF);
}
} else {
logger.debug(
"Heater {} is neither connected to a room nor marked as standalone. Someting is wrong, heater data: {}",
getThing().getUID(), heater);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
}
} else {
if (heater.canChangeTemp() && heater.getRoom() == null) {
updateIndependentHeaterProperties(command, null, null);
}
}
} else if (MillheatBindingConstants.CHANNEL_MASTER_SWITCH.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, heater.powerStatus() ? OnOffType.ON : OnOffType.OFF);
} else {
if (heater.canChangeTemp() && heater.getRoom() == null) {
updateIndependentHeaterProperties(null, command, null);
} else {
// Just overwrite with old state
updateState(channelUID, heater.powerStatus() ? OnOffType.ON : OnOffType.OFF);
}
}
} else {
logger.debug("Received command {} on channel {}, but this channel is not handled or supported by {}",
channelUID.getId(), command.toString(), this.getThing().getUID());
}
} else {
updateStatus(ThingStatus.OFFLINE);
}
}
private void updateIndependentHeaterProperties(@Nullable final Command temperatureCommand,
@Nullable final Command masterOnOffCommand, @Nullable final Command fanCommand) {
getAccountHandler().ifPresent(handler -> {
handler.updateIndependentHeaterProperties(config.macAddress, config.heaterId, temperatureCommand,
masterOnOffCommand, fanCommand);
});
}
@Override
public void initialize() {
config = getConfigAs(MillheatHeaterConfiguration.class);
logger.debug("Initializing Millheat heater using config {}", config);
if (config.heaterId == null && config.macAddress == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
} else {
final Optional<Heater> heater = getMillheatModel().findHeaterByMacOrId(config.macAddress, config.heaterId);
if (heater.isPresent()) {
addOptionalChannels(heater.get());
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE);
}
}
}
private void addOptionalChannels(final Heater heater) {
final List<Channel> newChannels = new ArrayList<>();
newChannels.addAll(getThing().getChannels());
if (heater.canChangeTemp() && heater.getRoom() == null) {
// Add power switch channel
newChannels
.add(ChannelBuilder
.create(new ChannelUID(getThing().getUID(), MillheatBindingConstants.CHANNEL_MASTER_SWITCH),
"Switch")
.withType(MillheatBindingConstants.CHANNEL_TYPE_MASTER_SWITCH_UID).build());
// Add independent heater target temperature
newChannels.add(ChannelBuilder
.create(new ChannelUID(getThing().getUID(), MillheatBindingConstants.CHANNEL_TARGET_TEMPERATURE),
"Number:Temperature")
.withType(MillheatBindingConstants.CHANNEL_TYPE_TARGET_TEMPERATURE_HEATER_UID).build());
}
updateThing(editThing().withChannels(newChannels).build());
}
}

View File

@@ -0,0 +1,135 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.handler;
import static org.openhab.binding.millheat.internal.MillheatBindingConstants.*;
import java.time.ZoneId;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.millheat.internal.config.MillheatHomeConfiguration;
import org.openhab.binding.millheat.internal.dto.SetHolidayParameterRequest;
import org.openhab.binding.millheat.internal.model.Home;
import org.openhab.binding.millheat.internal.model.MillheatModel;
import org.openhab.binding.millheat.internal.model.ModeType;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
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.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MillheatHomeHandler} is responsible for handling home commands, for now vacation mode properties
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class MillheatHomeHandler extends MillheatBaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(MillheatHomeHandler.class);
private @NonNullByDefault({}) MillheatHomeConfiguration config;
public MillheatHomeHandler(final Thing thing) {
super(thing);
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
handleCommand(channelUID, command, getMillheatModel());
}
@Override
protected void handleCommand(final ChannelUID channelUID, final Command command, final MillheatModel model) {
final Optional<Home> optionalHome = model.findHomeById(config.homeId);
if (optionalHome.isPresent()) {
updateStatus(ThingStatus.ONLINE);
final Home home = optionalHome.get();
if (CHANNEL_HOME_VACATION_TARGET_TEMPERATURE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, new QuantityType<>(home.getHolidayTemp(), SIUnits.CELSIUS));
} else if (command instanceof QuantityType<?>) {
updateVacationModeProperty(home, SetHolidayParameterRequest.PROP_TEMP, command);
} else if (command instanceof DecimalType) {
updateVacationModeProperty(home, SetHolidayParameterRequest.PROP_TEMP,
new QuantityType<>((DecimalType) command, SIUnits.CELSIUS));
}
} else if (CHANNEL_HOME_VACATION_MODE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, OnOffType.from(home.getMode().getMode() == ModeType.VACATION));
} else if (command instanceof OnOffType) {
updateVacationModeProperty(home, SetHolidayParameterRequest.PROP_MODE, command);
}
} else if (CHANNEL_HOME_VACATION_MODE_ADVANCED.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, OnOffType.from(home.isAdvancedVacationMode()));
} else if (command instanceof OnOffType) {
updateVacationModeProperty(home, SetHolidayParameterRequest.PROP_MODE_ADVANCED, command);
}
} else if (CHANNEL_HOME_VACATION_MODE_START.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
if (home.getVacationModeStart() != null) {
updateState(channelUID,
new DateTimeType(home.getVacationModeStart().atZone(ZoneId.systemDefault())));
} else {
updateState(channelUID, UnDefType.UNDEF);
}
} else if (command instanceof DateTimeType) {
updateVacationModeProperty(home, SetHolidayParameterRequest.PROP_START, command);
}
} else if (CHANNEL_HOME_VACATION_MODE_END.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
if (home.getVacationModeEnd() != null) {
updateState(channelUID,
new DateTimeType(home.getVacationModeEnd().atZone(ZoneId.systemDefault())));
} else {
updateState(channelUID, UnDefType.UNDEF);
}
} else if (command instanceof DateTimeType) {
updateVacationModeProperty(home, SetHolidayParameterRequest.PROP_END, command);
}
} else {
logger.debug("Received command {} on channel {}, but this channel is not handled or supported by {}",
channelUID.getId(), command.toString(), this.getThing().getUID());
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE);
}
}
private void updateVacationModeProperty(Home home, String property, Command command) {
getAccountHandler().ifPresent(handler -> {
handler.updateVacationProperty(home, property, command);
});
}
@Override
public void initialize() {
config = getConfigAs(MillheatHomeConfiguration.class);
logger.debug("Initializing Millheat home using config {}", config);
final Optional<Home> room = getMillheatModel().findHomeById(config.homeId);
if (room.isPresent()) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE);
}
}
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.handler;
import static org.openhab.binding.millheat.internal.MillheatBindingConstants.*;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.millheat.internal.config.MillheatRoomConfiguration;
import org.openhab.binding.millheat.internal.model.MillheatModel;
import org.openhab.binding.millheat.internal.model.ModeType;
import org.openhab.binding.millheat.internal.model.Room;
import org.openhab.core.library.types.OnOffType;
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.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.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MillheatRoomHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class MillheatRoomHandler extends MillheatBaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(MillheatRoomHandler.class);
private @NonNullByDefault({}) MillheatRoomConfiguration config;
public MillheatRoomHandler(final Thing thing) {
super(thing);
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
handleCommand(channelUID, command, getMillheatModel());
}
private void updateRoomTemperature(final Long roomId, final Command command, final ModeType modeType) {
getAccountHandler().ifPresent(handler -> {
handler.updateRoomTemperature(config.roomId, command, modeType);
});
}
@Override
protected void handleCommand(final ChannelUID channelUID, final Command command, final MillheatModel model) {
final Optional<Room> optionalRoom = model.findRoomById(config.roomId);
if (optionalRoom.isPresent()) {
updateStatus(ThingStatus.ONLINE);
final Room room = optionalRoom.get();
if (CHANNEL_CURRENT_TEMPERATURE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, new QuantityType<>(room.getCurrentTemp(), SIUnits.CELSIUS));
}
} else if (CHANNEL_CURRENT_MODE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, new StringType(room.getMode().toString()));
}
} else if (CHANNEL_PROGRAM.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, new StringType(room.getRoomProgramName()));
}
} else if (CHANNEL_COMFORT_TEMPERATURE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, new QuantityType<>(room.getComfortTemp(), SIUnits.CELSIUS));
} else {
updateRoomTemperature(config.roomId, command, ModeType.COMFORT);
}
} else if (CHANNEL_SLEEP_TEMPERATURE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, new QuantityType<>(room.getSleepTemp(), SIUnits.CELSIUS));
} else {
updateRoomTemperature(config.roomId, command, ModeType.SLEEP);
}
} else if (CHANNEL_AWAY_TEMPERATURE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, new QuantityType<>(room.getAwayTemp(), SIUnits.CELSIUS));
} else {
updateRoomTemperature(config.roomId, command, ModeType.AWAY);
}
} else if (CHANNEL_TARGET_TEMPERATURE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
final Integer targetTemperature = room.getTargetTemperature();
if (targetTemperature != null) {
updateState(channelUID, new QuantityType<>(targetTemperature, SIUnits.CELSIUS));
} else {
updateState(channelUID, UnDefType.UNDEF);
}
}
} else if (CHANNEL_HEATING_ACTIVE.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
updateState(channelUID, room.isHeatingActive() ? OnOffType.ON : OnOffType.OFF);
}
} else {
logger.debug("Received command {} on channel {}, but this channel is not handled or supported by {}",
channelUID.getId(), command.toString(), this.getThing().getUID());
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE);
}
}
@Override
public void initialize() {
config = getConfigAs(MillheatRoomConfiguration.class);
logger.debug("Initializing Millheat room using config {}", config);
final Optional<Room> room = getMillheatModel().findRoomById(config.roomId);
if (room.isPresent()) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE);
}
}
}

View File

@@ -0,0 +1,149 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.model;
import org.openhab.binding.millheat.internal.dto.DeviceDTO;
/**
* The {@link Heater} represents a heater, either connected to a room or independent
*
* @author Arne Seime - Initial contribution
*/
public class Heater {
private Room room;
private final Long id;
private final String name;
private final String macAddress;
private final boolean heatingActive;
private boolean canChangeTemp = true;
private final int subDomain;
private final int currentTemp;
private Integer targetTemp;
private boolean fanActive;
private boolean powerStatus;
private final boolean windowOpen;
public Heater(final DeviceDTO dto) {
id = dto.deviceId;
name = dto.deviceName;
macAddress = dto.macAddress;
heatingActive = dto.heaterFlag;
canChangeTemp = dto.holiday;
subDomain = dto.subDomainId;
currentTemp = (int) dto.currentTemp;
setTargetTemp(dto.holidayTemp);
setFanActive(dto.fanStatus);
setPowerStatus(dto.powerStatus);
windowOpen = dto.openWindow;
}
public Heater(final DeviceDTO dto, final Room room) {
this.room = room;
id = dto.deviceId;
name = dto.deviceName;
macAddress = dto.macAddress;
heatingActive = dto.heaterFlag;
canChangeTemp = dto.canChangeTemp;
subDomain = dto.subDomainId;
currentTemp = (int) dto.currentTemp;
if (room != null && room.getMode() != null) {
switch (room.getMode()) {
case COMFORT:
setTargetTemp(room.getComfortTemp());
break;
case SLEEP:
setTargetTemp(room.getSleepTemp());
break;
case AWAY:
setTargetTemp(room.getAwayTemp());
break;
case OFF:
setTargetTemp(null);
break;
default:
// NOOP
}
}
setFanActive(dto.fanStatus);
setPowerStatus(dto.powerStatus);
windowOpen = dto.openWindow;
}
@Override
public String toString() {
return "Heater [room=" + room + ", id=" + id + ", name=" + name + ", macAddress=" + macAddress
+ ", heatingActive=" + heatingActive + ", canChangeTemp=" + canChangeTemp + ", subDomain=" + subDomain
+ ", currentTemp=" + currentTemp + ", targetTemp=" + getTargetTemp() + ", fanActive=" + fanActive()
+ ", powerStatus=" + powerStatus() + ", windowOpen=" + windowOpen + "]";
}
public Room getRoom() {
return room;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getMacAddress() {
return macAddress;
}
public boolean isHeatingActive() {
return heatingActive;
}
public boolean canChangeTemp() {
return canChangeTemp;
}
public int getSubDomain() {
return subDomain;
}
public int getCurrentTemp() {
return currentTemp;
}
public Integer getTargetTemp() {
return targetTemp;
}
public boolean fanActive() {
return fanActive;
}
public boolean powerStatus() {
return powerStatus;
}
public boolean windowOpen() {
return windowOpen;
}
public void setTargetTemp(final Integer targetTemp) {
this.targetTemp = targetTemp;
}
public void setFanActive(final boolean fanActive) {
this.fanActive = fanActive;
}
public void setPowerStatus(final boolean powerStatus) {
this.powerStatus = powerStatus;
}
}

View File

@@ -0,0 +1,163 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.model;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import org.openhab.binding.millheat.internal.dto.HomeDTO;
import org.openhab.core.library.types.OnOffType;
/**
* The {@link Home} represents a home
*
* @author Arne Seime - Initial contribution
*/
public class Home {
private final long id;
private final String name;
private final int type;
private final String zoneOffset;
private int holidayTemp;
private Mode mode;
private final String program = null;
private final List<Room> rooms = new ArrayList<>();
private final List<Heater> independentHeaters = new ArrayList<>();
private LocalDateTime vacationModeStart;
private LocalDateTime vacationModeEnd;
private boolean advancedVacationMode;
public Home(final HomeDTO dto) {
id = dto.homeId;
name = dto.name;
type = dto.homeType;
zoneOffset = dto.timeZone;
holidayTemp = dto.holidayTemp;
advancedVacationMode = dto.holidayTempType == 0;
if (dto.holidayStartTime != 0) {
vacationModeStart = convertFromEpoch(dto.holidayStartTime);
}
if (dto.holidayEndTime != 0) {
vacationModeEnd = convertFromEpoch(dto.holidayEndTime);
}
if (dto.holiday) {
mode = new Mode(ModeType.VACATION, vacationModeStart, vacationModeEnd);
} else if (dto.alwaysHome) {
mode = new Mode(ModeType.ALWAYSHOME, null, null);
} else {
final LocalDateTime modeStart = LocalDateTime.ofEpochSecond(dto.modeStartTime, 0,
ZoneOffset.of(zoneOffset));
final LocalDateTime modeEnd = modeStart.withHour(dto.modeHour).withMinute(dto.modeMinute);
mode = new Mode(ModeType.valueOf(dto.currentMode), modeStart, modeEnd);
}
}
private LocalDateTime convertFromEpoch(long epoch) {
return LocalDateTime.ofEpochSecond(epoch, 0, ZoneOffset.of(zoneOffset));
}
public void addRoom(final Room room) {
rooms.add(room);
}
public void addHeater(final Heater heater) {
independentHeaters.add(heater);
}
@Override
public String toString() {
return "Home [id=" + id + ", name=" + name + ", type=" + type + ", zoneOffset=" + zoneOffset + ", holidayTemp="
+ holidayTemp + ", mode=" + mode + ", rooms=" + rooms + ", independentHeaters=" + independentHeaters
+ ", program=" + program + "]";
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public int getType() {
return type;
}
public String getTimezone() {
return zoneOffset;
}
public int getHolidayTemp() {
return holidayTemp;
}
public Mode getMode() {
return mode;
}
public String getProgram() {
return program;
}
public List<Room> getRooms() {
return rooms;
}
public List<Heater> getIndependentHeaters() {
return independentHeaters;
}
public LocalDateTime getVacationModeStart() {
return vacationModeStart;
}
public LocalDateTime getVacationModeEnd() {
return vacationModeEnd;
}
public void setVacationModeStart(long epoch) {
vacationModeStart = convertFromEpoch(epoch);
updateVacationMode();
}
public void setVacationModeEnd(long epoch) {
vacationModeEnd = convertFromEpoch(epoch);
updateVacationMode();
}
public void setHolidayTemp(int holidayTemp) {
this.holidayTemp = holidayTemp;
updateVacationMode();
}
private void updateVacationMode() {
if (mode.getMode() == ModeType.VACATION) {
mode = new Mode(ModeType.VACATION, vacationModeStart, vacationModeEnd);
}
}
public void setVacationModeAdvanced(OnOffType command) {
advancedVacationMode = (OnOffType.ON == command);
}
public boolean isAdvancedVacationMode() {
return advancedVacationMode;
}
public void setAdvancedVacationMode(boolean advancedVacationMode) {
this.advancedVacationMode = advancedVacationMode;
}
}

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link MillheatModel} represents the home structure as designed by the user in the Millheat app.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class MillheatModel {
private final long lastUpdated;
private final List<Home> homes = new ArrayList<>();
public MillheatModel(final long lastUpdated) {
this.lastUpdated = lastUpdated;
}
public void addHome(final Home home) {
homes.add(home);
}
public List<Home> getHomes() {
return homes;
}
public long getLastUpdated() {
return lastUpdated;
}
public Optional<Heater> findHeaterById(final Long id) {
return findHeaters().filter(heater -> id.equals(heater.getId())).findFirst();
}
public Optional<Heater> findHeaterByMac(final String macAddress) {
return findHeaters().filter(heater -> macAddress.equals(heater.getMacAddress())).findFirst();
}
public Optional<Heater> findHeaterByMacOrId(@Nullable final String macAddress, @Nullable final Long id) {
Optional<Heater> heater = Optional.empty();
if (macAddress != null) {
heater = findHeaterByMac(macAddress);
}
if (!heater.isPresent() && id != null) {
heater = findHeaterById(id);
}
return heater;
}
private Stream<Heater> findHeaters() {
return Stream.concat(
homes.stream().flatMap(home -> home.getRooms().stream()).flatMap(room -> room.getHeaters().stream()),
homes.stream().flatMap(room -> room.getIndependentHeaters().stream()));
}
public Optional<Room> findRoomById(final Long id) {
return homes.stream().flatMap(home -> home.getRooms().stream()).filter(room -> id.equals(room.getId()))
.findFirst();
}
public Optional<Home> findHomeByRoomId(final Long id) {
for (final Home home : homes) {
for (final Room room : home.getRooms()) {
if (id.equals(room.getId())) {
return Optional.of(home);
}
}
}
return Optional.empty();
}
public Optional<Home> findHomeById(Long homeId) {
return homes.stream().filter(e -> e.getId().equals(homeId)).findFirst();
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.model;
import java.time.LocalDateTime;
/**
* The {@link Mode} represents a mode with start and end time
*
* @author Arne Seime - Initial contribution
*/
public class Mode {
private final ModeType mode;
private final LocalDateTime start;
private final LocalDateTime end;
public Mode(final ModeType mode, final LocalDateTime start, final LocalDateTime end) {
this.mode = mode;
this.start = start;
this.end = end;
}
public ModeType getMode() {
return mode;
}
public LocalDateTime getStart() {
return start;
}
public LocalDateTime getEnd() {
return end;
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.model;
/**
* The {@link ModeType} represents a type of mode the user can set in the app.
*
* @author Arne Seime - Initial contribution
*/
public enum ModeType {
ALWAYSHOME(-1),
COMFORT(1),
SLEEP(2),
AWAY(3),
VACATION(4),
OFF(5);
public static ModeType valueOf(final int modeVal) {
for (final ModeType mode : ModeType.values()) {
if (mode.value == modeVal) {
return mode;
}
}
return null;
}
private final int value;
ModeType(final int value) {
this.value = value;
}
public int getValue() {
return value;
}
}

View File

@@ -0,0 +1,123 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal.model;
import java.util.ArrayList;
import java.util.List;
import org.openhab.binding.millheat.internal.dto.RoomDTO;
/**
* The {@link Room} represents a room in a home as designed by the end user in the Millheat app.
*
* @author Arne Seime - Initial contribution
*/
public class Room {
private final Home home;
private final long id;
private final String name;
private final int currentTemp;
private final int comfortTemp;
private final int sleepTemp;
private final int awayTemp;
private final boolean heatingActive;
private final ModeType mode;
private final String roomProgramName;
private final List<Heater> heaters = new ArrayList<>();
public Room(final RoomDTO dto, final Home home) {
this.home = home;
id = dto.roomId;
name = dto.name;
currentTemp = (int) dto.currentTemp;
comfortTemp = dto.comfortTemp;
sleepTemp = dto.sleepTemp;
awayTemp = dto.awayTemp;
heatingActive = dto.heatStatus;
mode = ModeType.valueOf(dto.currentMode);
roomProgramName = dto.roomProgram;
}
public void addHeater(final Heater h) {
heaters.add(h);
}
public List<Heater> getHeaters() {
return heaters;
}
public Integer getTargetTemperature() {
switch (mode) {
case VACATION:
return home.getHolidayTemp();
case SLEEP:
return sleepTemp;
case COMFORT:
return comfortTemp;
case AWAY:
return awayTemp;
case OFF:
case ALWAYSHOME:
default:
return null;
}
}
@Override
public String toString() {
return "Room [home=" + home.getId() + ", id=" + id + ", name=" + name + ", currentTemp=" + currentTemp
+ ", comfortTemp=" + comfortTemp + ", sleepTemp=" + sleepTemp + ", awayTemp=" + awayTemp
+ ", heatingActive=" + heatingActive + ", mode=" + mode + ", roomProgramName=" + roomProgramName
+ ", heaters=" + heaters + "]";
}
public Home getHome() {
return home;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public int getCurrentTemp() {
return currentTemp;
}
public int getComfortTemp() {
return comfortTemp;
}
public int getSleepTemp() {
return sleepTemp;
}
public int getAwayTemp() {
return awayTemp;
}
public boolean isHeatingActive() {
return heatingActive;
}
public ModeType getMode() {
return mode;
}
public String getRoomProgramName() {
return roomProgramName;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="millheat" 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>Millheat Binding</name>
<description>This is the binding for Mill Heat Wi-Fi enabled heaters. See https://www.millheat.com/mill-wifi/</description>
<author>Arne Seime</author>
</binding:binding>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:millheat:account">
<parameter name="username" type="text" required="true">
<label>Username</label>
<description>Your Millheat app username (email)</description>
</parameter>
<parameter name="password" type="text" required="true">
<label>Password</label>
<description>Your Millheat app password</description>
<context>password</context>
</parameter>
<parameter name="refreshInterval" type="integer" min="30" unit="s">
<label>Refresh Interval</label>
<description>Specifies the refresh time in seconds for polling temperature updates from Millheat service</description>
<default>120</default>
</parameter>
</config-description>
<config-description uri="thing-type:millheat:heater">
<parameter name="macAddress" type="text">
<label>MAC Address</label>
<description>Either MAC address or heaterId is required</description>
</parameter>
<parameter name="heaterId" type="integer">
<label>Heater ID</label>
<description>Either MAC address or heaterId is required</description>
</parameter>
<parameter name="power" type="integer">
<label>Heating Power</label>
<description>Number of watts this heater is consuming when it is heating. This value is sent to the currentPower
channel when the heater is heating in order to track energy usage</description>
</parameter>
</config-description>
<config-description uri="thing-type:millheat:room">
<parameter name="roomId" type="integer" required="true">
<label>Room ID</label>
</parameter>
</config-description>
<config-description uri="thing-type:millheat:home">
<parameter name="homeId" type="integer" required="true">
<label>Home ID</label>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="millheat"
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="account">
<label>Mill Heating API</label>
<description>This bridge represents the gateway to Mill Heating API</description>
<config-description-ref uri="thing-type:millheat:account"/>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="millheat"
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">
<channel-type id="currentTemperature">
<item-type>Number:Temperature</item-type>
<label>Current Temperature</label>
<category>Temperature</category>
<tags>
<tag>CurrentTemperature</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="comfortTemperature">
<item-type>Number:Temperature</item-type>
<label>Temperature Comfort Mode</label>
<category>Heating</category>
<tags>
<tag>TargetTemperature</tag>
</tags>
<state pattern="%d %unit%" min="5" max="35" step="1"/>
</channel-type>
<channel-type id="sleepTemperature">
<item-type>Number:Temperature</item-type>
<label>Temperature Sleep Mode</label>
<category>Heating</category>
<tags>
<tag>TargetTemperature</tag>
</tags>
<state pattern="%d %unit%" min="5" max="35" step="1"/>
</channel-type>
<channel-type id="awayTemperature">
<item-type>Number:Temperature</item-type>
<label>Temperature Away Mode</label>
<description>Set temperature away mode</description>
<category>Heating</category>
<tags>
<tag>TargetTemperature</tag>
</tags>
<state pattern="%d %unit%" min="5" max="35" step="1"/>
</channel-type>
<channel-type id="targetTemperatureHeater">
<item-type>Number:Temperature</item-type>
<label>Target Temperature</label>
<category>Heating</category>
<tags>
<tag>TargetTemperature</tag>
</tags>
<state pattern="%d %unit%" min="5" max="35" step="1"/>
</channel-type>
<channel-type id="targetTemperatureRoom">
<item-type>Number:Temperature</item-type>
<label>Target Temperature</label>
<category>Heating</category>
<tags>
<tag>TargetTemperature</tag>
</tags>
<state pattern="%d %unit%" readOnly="true" min="5" max="35" step="1"/>
</channel-type>
<channel-type id="heatingActive">
<item-type>Switch</item-type>
<label>Heating Active</label>
<description>Current state of the heater or heaters in room</description>
<category>Energy</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="independent">
<item-type>Switch</item-type>
<label>Independent Heater</label>
<description>ON if heater is an independent heater and not connected to a room</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="masterSwitch">
<item-type>Switch</item-type>
<label>Master Switch</label>
<description>Master ON/OFF switch for independent heater</description>
</channel-type>
<channel-type id="fanActive">
<item-type>Switch</item-type>
<label>Fan Active</label>
<description>Current state of heater fan (if available, OFF if not found)</description>
<category>Flow</category>
</channel-type>
<channel-type id="window">
<item-type>Contact</item-type>
<label>Window State</label>
<description>Open window/cold air flow detection</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="currentEnergy">
<item-type>Number:Power</item-type>
<label>Energy Usage</label>
<description>Actual energy usage in watts</description>
<category>Energy</category>
<state readOnly="true" pattern="%d W"></state>
</channel-type>
<channel-type id="currentMode">
<item-type>String</item-type>
<label>Current Room Program Mode</label>
<state readOnly="true">
<options>
<option value="Comfort">Comfort</option>
<option value="Sleep">Sleep</option>
<option value="Away">Away</option>
<option value="Off">Off</option>
<option value="AdvancedAway">Vacation away</option>
</options>
</state>
</channel-type>
<channel-type id="program">
<item-type>String</item-type>
<label>Program</label>
<description>Program associated with room</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="vacationModeTargetTemperature">
<item-type>Number:Temperature</item-type>
<label>Target Temperature Vacation</label>
<category>Heating</category>
<tags>
<tag>TargetTemperature</tag>
</tags>
<state pattern="%d %unit%" min="5" max="35" step="1"/>
</channel-type>
<channel-type id="vacationMode">
<item-type>Switch</item-type>
<label>Vacation Mode</label>
<description>Toggles vacation mode. Start and end time must be preset before activating</description>
</channel-type>
<channel-type id="vacationModeAdvanced">
<item-type>Switch</item-type>
<label>Advanced Vacation Mode</label>
<description>Use room Away mode temperatures instead of home global temperature</description>
</channel-type>
<channel-type id="vacationModeStart">
<item-type>DateTime</item-type>
<label>Start of Vacation</label>
</channel-type>
<channel-type id="vacationModeEnd">
<item-type>DateTime</item-type>
<label>End of Vacation</label>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="millheat"
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">
<thing-type id="heater">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Wi-Fi Enabled Heater</label>
<channels>
<channel id="currentTemperature" typeId="currentTemperature"/>
<channel id="heatingActive" typeId="heatingActive"/>
<channel id="fanActive" typeId="fanActive"/>
<channel id="currentEnergy" typeId="currentEnergy"/>
<channel id="independent" typeId="independent"/>
<channel id="window" typeId="window"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:millheat:heater"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="millheat"
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">
<thing-type id="home">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Home</label>
<channels>
<channel id="vacationModeTargetTemperature" typeId="vacationModeTargetTemperature"/>
<channel id="vacationMode" typeId="vacationMode"/>
<channel id="vacationModeAdvanced" typeId="vacationModeAdvanced"/>
<channel id="vacationModeStart" typeId="vacationModeStart"/>
<channel id="vacationModeEnd" typeId="vacationModeEnd"/>
</channels>
<representation-property>homeId</representation-property>
<config-description-ref uri="thing-type:millheat:home"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="millheat"
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">
<thing-type id="room">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Room with one or more Wi-Fi enabled heaters</label>
<channels>
<channel id="currentTemperature" typeId="currentTemperature"/>
<channel id="targetTemperature" typeId="targetTemperatureRoom"/>
<channel id="comfortTemperature" typeId="comfortTemperature"/>
<channel id="sleepTemperature" typeId="sleepTemperature"/>
<channel id="awayTemperature" typeId="awayTemperature"/>
<channel id="heatingActive" typeId="heatingActive"/>
<channel id="currentMode" typeId="currentMode"/>
<channel id="program" typeId="program"/>
</channels>
<representation-property>roomId</representation-property>
<config-description-ref uri="thing-type:millheat:room"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,109 @@
/**
* Copyright (c) 2010-2020 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.millheat.internal;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import java.io.IOException;
import org.apache.commons.io.IOUtils;
import org.eclipse.jetty.client.HttpClient;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.openhab.binding.millheat.internal.config.MillheatAccountConfiguration;
import org.openhab.binding.millheat.internal.handler.MillheatAccountHandler;
import org.openhab.binding.millheat.internal.model.MillheatModel;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
import org.osgi.framework.BundleContext;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
/**
* @author Arne Seime - Initial contribution
*/
public class MillHeatAccountHandlerTest {
@Rule
public WireMockRule wireMockRule = new WireMockRule(WireMockConfiguration.options().dynamicPort());
@Mock
private Bridge millheatAccountMock;
private HttpClient httpClient;
@Mock
private Configuration configuration;
@Mock
private BundleContext bundleContext;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
httpClient = new HttpClient();
httpClient.start();
MillheatAccountHandler.authEndpoint = "http://localhost:" + wireMockRule.port() + "/zc-account/v1/";
MillheatAccountHandler.serviceEndpoint = "http://localhost:" + wireMockRule.port() + "/millService/v1/";
}
@After
public void shutdown() throws Exception {
httpClient.stop();
}
@Test
public void testUpdateModel() throws InterruptedException, IOException, MillheatCommunicationException {
final String getHomesResponse = IOUtils.toString(getClass().getResourceAsStream("/select_home_list_ok.json"));
final String getRoomsByHomeResponse = IOUtils
.toString(getClass().getResourceAsStream("/get_rooms_by_home_ok.json"));
final String getDeviceByRoomResponse = IOUtils
.toString(getClass().getResourceAsStream("/get_device_by_room_ok.json"));
final String getIndependentDevicesResponse = IOUtils
.toString(getClass().getResourceAsStream("/get_independent_devices_ok.json"));
stubFor(post(urlEqualTo("/millService/v1/selectHomeList"))
.willReturn(aResponse().withStatus(200).withBody(getHomesResponse)));
stubFor(post(urlEqualTo("/millService/v1/selectRoombyHome"))
.willReturn(aResponse().withStatus(200).withBody(getRoomsByHomeResponse)));
stubFor(post(urlEqualTo("/millService/v1/selectDevicebyRoom"))
.willReturn(aResponse().withStatus(200).withBody(getDeviceByRoomResponse)));
stubFor(post(urlEqualTo("/millService/v1/getIndependentDevices"))
.willReturn(aResponse().withStatus(200).withBody(getIndependentDevicesResponse)));
when(millheatAccountMock.getConfiguration()).thenReturn(configuration);
when(millheatAccountMock.getUID()).thenReturn(new ThingUID("millheat:account:thinguid"));
final MillheatAccountConfiguration accountConfig = new MillheatAccountConfiguration();
accountConfig.username = "username";
accountConfig.password = "password";
when(configuration.as(eq(MillheatAccountConfiguration.class))).thenReturn(accountConfig);
final MillheatAccountHandler subject = new MillheatAccountHandler(millheatAccountMock, httpClient,
bundleContext);
MillheatModel model = subject.refreshModel();
Assert.assertEquals(1, model.getHomes().size());
verify(postRequestedFor(urlMatching("/millService/v1/selectHomeList")));
verify(postRequestedFor(urlMatching("/millService/v1/selectRoombyHome")));
verify(postRequestedFor(urlMatching("/millService/v1/selectDevicebyRoom")));
verify(postRequestedFor(urlMatching("/millService/v1/getIndependentDevices")));
}
}

View File

@@ -0,0 +1,36 @@
{
"always": 0,
"backMinute": 0,
"roomProgramId": 3242342342324,
"controlSource": "0,0,0",
"comfortTemp": 20,
"roomProgram": "Kontor",
"awayTemp": 10,
"holidayTemp": 10,
"avgTemp": 15.0,
"roomId": 23423423423423,
"roomName": "Kontor",
"deviceInfo": [
{
"heaterFlag": 0,
"subDomainId": 242424,
"controlType": 0,
"currentTemp": 15.0,
"canChangeTemp": 0,
"deviceId": 12334,
"deviceName": "Kontor",
"mac": "F0XXXXXXXXX",
"deviceStatus": 0
}
],
"backHour": 0,
"currentMode": 4,
"heatStatus": 0,
"offLineDeviceNum": 0,
"total": 1,
"independentCount": 0,
"sleepTemp": 13,
"onlineDeviceNum": 1,
"isOffline": 1,
"programMode": 0
}

View File

@@ -0,0 +1,3 @@
{
"deviceInfo": []
}

View File

@@ -0,0 +1,81 @@
{
"backMinute": 0,
"offLineDeviceNum": 0,
"mode": 0,
"homeAlways": 0,
"homeName": "Hjemme",
"isHoliday": 0,
"onlineDeviceNum": 3,
"programList": [
{
"programName": "Standard Program",
"homeId": 20190260000,
"programId": 20190260000
},
{
"programName": "Barnerom",
"homeId": 20190260000,
"programId": 20171231209984
},
{
"programName": "Kontor",
"homeId": 20190260000,
"programId": 2019129984
}
],
"homeType": 0,
"backHour": 0,
"roomInfo": [
{
"controlSource": "0,0,0",
"comfortTemp": 20,
"roomProgram": "Barnerom",
"awayTemp": 10,
"avgTemp": 19.0,
"roomId": 201900000,
"roomName": "Bedroom1",
"currentMode": 2,
"heatStatus": 0,
"offLineDeviceNum": 0,
"total": 1,
"independentCount": 0,
"sleepTemp": 18,
"onlineDeviceNum": 1,
"isOffline": 1
},
{
"controlSource": "0,0,0",
"comfortTemp": 20,
"roomProgram": "Barnerom",
"awayTemp": 10,
"avgTemp": 19.0,
"roomId": 20190207000,
"roomName": "Bedroom2",
"currentMode": 2,
"heatStatus": 0,
"offLineDeviceNum": 0,
"total": 1,
"independentCount": 0,
"sleepTemp": 17,
"onlineDeviceNum": 1,
"isOffline": 1
},
{
"controlSource": "0,0,0",
"comfortTemp": 20,
"roomProgram": "Kontor",
"awayTemp": 10,
"avgTemp": 16.0,
"roomId": 20190000,
"roomName": "Kontor",
"currentMode": 5,
"heatStatus": 0,
"offLineDeviceNum": 0,
"total": 1,
"independentCount": 0,
"sleepTemp": 13,
"onlineDeviceNum": 1,
"isOffline": 1
}
]
}

View File

@@ -0,0 +1,5 @@
{
"description": "password is not set",
"error": "invalid param",
"errorCode": 3002
}

View File

@@ -0,0 +1,14 @@
{
"email": "email@gmail.com",
"nickName": "Nikky",
"phone": "",
"refreshToken": "refreshToken",
"refreshTokenExpire": "2019-03-20 18:13:30",
"token": "token",
"tokenExpire": "2019-02-18 20:13:30",
"userId": 234324,
"userProfile": {
"nick_name": "Nikky",
"privacyPolicy": 1
}
}

View File

@@ -0,0 +1,21 @@
{
"hourSystem": 1,
"homeList": [
{
"homeAlways": 0,
"homeName": "Hjemme",
"isHoliday": 0,
"holidayStartTime": 1549145440,
"timeZone": "+01:00",
"modeMinute": 0,
"modeStartTime": 0,
"holidayTemp": 10,
"modeHour": 0,
"currentMode": 0,
"holidayEndTime": 1549875200,
"homeType": 0,
"homeId": 2019000,
"programId": 201960000
}
]
}