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 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="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 kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.sensibo</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/openhab2-addons

View File

@@ -0,0 +1,83 @@
# Sensibo Binding
This binding integrates the Sensibo Sky aircondition remote control
See https://www.sensibo.com/
## Supported Things
This binding supports Sensibo Sky only.
* `account` = Sensibo API - the account bridge
* `sensibosky` = Sensibo Sky remote control
## Discovery
In order to do discovery, add a thing of type Sensibo API and add the API key.
API key can be obtained here: https://home.sensibo.com/me/api
## Thing Configuration
See full example below for how to configure using thing files.
### Account
* `apiKey` = API key obtained here: https://home.sensibo.com/me/api
* `refreshInterval` = number of seconds between refresh calls to the server
### Sensibo Sky
* `macAddress` = network mac address of device.
Can be found printed on the back of the device
Or you can find it during discovery.
## Channels
### Sensibo Sky
| Channel | Read/write | Item type | Description |
| ------------------- | ------------- | --------------------- | ----------- |
| currentTemperature | R | Number:Temperature | Measured temperature |
| currentHumidity | R | Number:Dimensionless | Measured relative humidity, reported in percent |
| targetTemperature | R/W | Number:Temperature | Current target temperature for this room |
| masterSwitch | R/W | Switch | Switch AC ON or OFF |
| mode | R/W | String | Current mode (cool, heat, etc, actual modes provided provided by the API) being active |
| fanLevel | R/W | String | Current fan level (low, auto etc, actual levels provided provided by the API |
| swingMode | R/W | String | Current swing mode (actual modes provided provided by the API |
| timer | R/W | Number | Number of seconds until AC is switched off automatically. Setting to a value less than 60 seconds will cancel timer |
## Full Example
sensibo.things:
```
Bridge sensibo:account:home "Sensibo account" [apiKey="XYZASDASDAD", refreshInterval=120] {
Thing sensibosky office "Sensibo Sky Office" [ macAddress="001122334455" ]
}
```
sensibo.items:
```
Number:Temperature AC_Office_Room_Current_Temperature "Temperature [%.1f %unit%]" <temperature> {channel="sensibo:sensibosky:home:office:currentTemperature"}
Number:Dimensionless AC_Office_Room_Current_Humidity "Relative humidity [%.1f %%]" <humidity > {channel="sensibo:sensibosky:home:office:currentHumidity"}
Number:Temperature AC_Office_Room_Target_Temperature "Target temperature [%d %unit%]" <temperature> {channel="sensibo:sensibosky:home:office:targetTemperature"}
String AC_Office_Room_Mode "AC mode [%s]" {channel="sensibo:sensibosky:home:office:mode"}
String AC_Office_Room_Swing_Mode "AC swing mode [%s]" {channel="sensibo:sensibosky:home:office:swingMode"}
Switch AC_Office_Heater_MasterSwitch "AC power [%s]" <switch> {channel="sensibo:sensibosky:home:office:masterSwitch"}
String AC_Office_Heater_Fan_Level "Fan level [%s]" <fan> {channel="sensibo:sensibosky:home:office:fanLevel"}
Number AC_Office_Heater_Timer "Timer seconds [%d]" <timer> {channel="sensibo:sensibosky:home:office:timer"}
```
sitemap:
```
Switch item=AC_Office_Heater_MasterSwitch
Selection item=AC_Office_Room_Mode
Setpoint item=AC_Office_Room_Target_Temperature
Selection item=AC_Office_Heater_Fan_Level
Selection item=AC_Office_Room_Swing_Mode
Text item=AC_Office_Room_Current_Temperature
Text item=AC_Office_Room_Current_Humidity
```

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.sensibo</artifactId>
<name>openHAB Add-ons :: Bundles :: Sensibo 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.sensibo-${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-sensibo" description="Sensibo Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.sensibo/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,73 @@
/**
* 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.sensibo.internal;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.sensibo.internal.handler.SensiboSkyHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.type.ChannelGroupType;
import org.openhab.core.thing.type.ChannelGroupTypeProvider;
import org.openhab.core.thing.type.ChannelGroupTypeUID;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* Channel Type Provider that does a callback the SensiboSkyHandler that initiated it.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class CallbackChannelsTypeProvider
implements ChannelTypeProvider, ChannelGroupTypeProvider, ThingHandlerService {
private @NonNullByDefault({}) SensiboSkyHandler handler;
@Override
public Collection<ChannelType> getChannelTypes(@Nullable final Locale locale) {
return handler.getChannelTypes(locale);
}
@Override
public @Nullable ChannelType getChannelType(final ChannelTypeUID channelTypeUID, @Nullable final Locale locale) {
return handler.getChannelType(channelTypeUID, locale);
}
@Override
public @Nullable ChannelGroupType getChannelGroupType(final ChannelGroupTypeUID channelGroupTypeUID,
@Nullable final Locale locale) {
return null;
}
@Override
public Collection<ChannelGroupType> getChannelGroupTypes(@Nullable final Locale locale) {
return Collections.emptyList();
}
@Override
@Nullable
public ThingHandler getThingHandler() {
return handler;
}
@NonNullByDefault({})
@Override
public void setThingHandler(final ThingHandler handler) {
this.handler = (SensiboSkyHandler) handler;
}
}

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.sensibo.internal;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SensiboBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class SensiboBindingConstants {
public static final String BINDING_ID = "sensibo";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_SENSIBOSKY = new ThingTypeUID(BINDING_ID, "sensibosky");
// Fixed channels
public static final String CHANNEL_CURRENT_TEMPERATURE = "currentTemperature";
public static final String CHANNEL_CURRENT_HUMIDITY = "currentHumidity";
public static final String CHANNEL_MASTER_SWITCH = "masterSwitch";
public static final String CHANNEL_TIMER = "timer";
// Dynamic channels
public static final String CHANNEL_FAN_LEVEL = "fanLevel";
public static final String CHANNEL_MODE = "mode";
public static final String CHANNEL_SWING_MODE = "swingMode";
public static final String CHANNEL_TARGET_TEMPERATURE = "targetTemperature";
public static final String CHANNEL_TYPE_FAN_LEVEL = "fanLevel";
public static final String CHANNEL_TYPE_MODE = "mode";
public static final String CHANNEL_TYPE_SWING_MODE = "swing";
public static final String CHANNEL_TYPE_TARGET_TEMPERATURE = "targetTemperature";
public static final Set<String> DYNAMIC_CHANNEL_TYPES = Collections.unmodifiableSet(Stream
.of(CHANNEL_TYPE_FAN_LEVEL, CHANNEL_TYPE_MODE, CHANNEL_TYPE_SWING_MODE, CHANNEL_TYPE_TARGET_TEMPERATURE)
.collect(Collectors.toSet()));
}

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.sensibo.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.sensibo.internal.dto.AbstractRequest;
/**
* The {@link SensiboCommunicationException} class wraps exceptions raised when communicating with the API
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class SensiboCommunicationException extends SensiboException {
private static final long serialVersionUID = 1L;
public SensiboCommunicationException(final String message, final Throwable cause) {
super(message, cause);
}
public SensiboCommunicationException(final String message) {
super(message);
}
public SensiboCommunicationException(final AbstractRequest req, final String overallStatus) {
super("Server responded with error to request " + req.getClass().getSimpleName() + "/" + req.getRequestUrl()
+ ": " + overallStatus);
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.sensibo.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SensiboConfigurationException} class wraps exceptions raised when due to configuration errors
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class SensiboConfigurationException extends SensiboException {
private static final long serialVersionUID = 1L;
public SensiboConfigurationException(final String message) {
super(message);
}
}

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.sensibo.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SensiboException} class wraps exceptions raised when communicating with the API
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public abstract class SensiboException extends Exception {
private static final long serialVersionUID = 1L;
public SensiboException(String message) {
super(message);
}
public SensiboException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,103 @@
/**
* 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.sensibo.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.sensibo.internal.discovery.SensiboDiscoveryService;
import org.openhab.binding.sensibo.internal.handler.SensiboAccountHandler;
import org.openhab.binding.sensibo.internal.handler.SensiboSkyHandler;
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 SensiboHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.sensibo", service = ThingHandlerFactory.class)
public class SensiboHandlerFactory extends BaseThingHandlerFactory {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.of(SensiboBindingConstants.THING_TYPE_ACCOUNT, SensiboBindingConstants.THING_TYPE_SENSIBOSKY)
.collect(Collectors.toSet()));
private final HttpClient httpClient;
private Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
@Activate
public SensiboHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
protected @Nullable ThingHandler createHandler(final Thing thing) {
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (SensiboBindingConstants.THING_TYPE_SENSIBOSKY.equals(thingTypeUID)) {
return new SensiboSkyHandler(thing);
} else if (SensiboBindingConstants.THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
final SensiboAccountHandler handler = new SensiboAccountHandler((Bridge) thing, httpClient);
registerDeviceDiscoveryService(handler);
return handler;
}
return null;
}
private void registerDeviceDiscoveryService(SensiboAccountHandler bridgeHandler) {
SensiboDiscoveryService discoveryService = new SensiboDiscoveryService(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);
}
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof SensiboAccountHandler) {
ThingUID thingUID = thingHandler.getThing().getUID();
unregisterDeviceDiscoveryService(thingUID);
}
super.removeHandler(thingHandler);
}
@Override
public boolean supportsThingType(final ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
}

View File

@@ -0,0 +1,53 @@
/**
* 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.sensibo.internal;
import javax.measure.Unit;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
/**
* The {@link SensiboTemperatureUnitConverter} converts to/from Sensibo temperature symbols to Unit<Temperature>
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public final class SensiboTemperatureUnitConverter {
public static Unit<Temperature> parseFromSensiboFormat(@Nullable String symbol) {
if (symbol == null) {
symbol = "C";
}
switch (symbol) {
case "C":
return SIUnits.CELSIUS;
case "F":
return ImperialUnits.FAHRENHEIT;
default:
throw new IllegalArgumentException("Do not understand temperature unit " + symbol);
}
}
public static String toSensiboFormat(@Nullable Unit<Temperature> unit) {
if (SIUnits.CELSIUS.equals(unit)) {
return "C";
} else if (ImperialUnits.FAHRENHEIT.equals(unit)) {
return "F";
} else {
throw new IllegalArgumentException("Do not understand temperature unit " + unit);
}
}
}

View File

@@ -0,0 +1,143 @@
/**
* 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.sensibo.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 openHAB 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) {
parser = new JsonParser();
this.gson = gson;
this.prefix = prefix;
}
private void dump(final Request request, String[] stringsToRemove) {
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(getCharset(theRequest.getHeaders()).decode(content).toString()));
request.onRequestSuccess(theRequest -> {
if (contentBuffer.length() > 0) {
group.append("\n");
group.append(reformatJson(contentBuffer.toString()));
}
String dataToLog = group.toString();
scrambleAndLog(stringsToRemove, 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(getCharset(theResponse.getHeaders()).decode(content).toString()));
request.onResponseSuccess(theResponse -> {
if (contentBuffer.length() > 0) {
group.append("\n");
group.append(reformatJson(contentBuffer.toString()));
}
String dataToLog = group.toString();
scrambleAndLog(stringsToRemove, dataToLog);
});
}
}
private void scrambleAndLog(String[] stringsToRemove, String dataToLog) {
String modifiedData = dataToLog;
for (String stringToRemove : stringsToRemove) {
modifiedData = modifiedData.replace(stringToRemove, "<HIDDEN>");
}
logger.debug("{}", modifiedData);
}
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, String[] stringToRemove) {
dump(request, stringToRemove);
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,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.sensibo.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link SensiboAccountConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class SensiboAccountConfiguration {
/**
* API key from https://home.sensibo.com/me/api
*/
@Nullable
public String apiKey;
public int refreshInterval = 120;
@Override
public String toString() {
return "SensiboAccountConfiguration [apiKey=<not showing>, refreshInterval=" + refreshInterval + "]";
}
}

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.sensibo.internal.config;
/**
* The {@link SensiboSkyConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Arne Seime - Initial contribution
*/
public class SensiboSkyConfiguration {
/*
* SensiboSky MAC address
*/
public String macAddress;
@Override
public String toString() {
return "SensiboSkyConfiguration [macAddress=" + macAddress + "]";
}
}

View File

@@ -0,0 +1,102 @@
/**
* 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.sensibo.internal.discovery;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.sensibo.internal.SensiboBindingConstants;
import org.openhab.binding.sensibo.internal.handler.SensiboAccountHandler;
import org.openhab.binding.sensibo.internal.model.SensiboModel;
import org.openhab.binding.sensibo.internal.model.SensiboSky;
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.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class SensiboDiscoveryService extends AbstractDiscoveryService {
public static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Collections
.singleton(SensiboBindingConstants.THING_TYPE_SENSIBOSKY);
private static final long REFRESH_INTERVAL_MINUTES = 60;
private final Logger logger = LoggerFactory.getLogger(SensiboDiscoveryService.class);
private final SensiboAccountHandler accountHandler;
private Optional<ScheduledFuture<?>> discoveryJob = Optional.empty();
public SensiboDiscoveryService(final SensiboAccountHandler accountHandler) {
super(DISCOVERABLE_THING_TYPES_UIDS, 10);
this.accountHandler = accountHandler;
}
@Override
protected void startBackgroundDiscovery() {
discoveryJob = Optional
.of(scheduler.scheduleWithFixedDelay(this::startScan, 0, REFRESH_INTERVAL_MINUTES, TimeUnit.MINUTES));
}
@Override
protected void startScan() {
logger.debug("Start scan for Sensibo devices.");
synchronized (this) {
removeOlderResults(getTimestampOfLastScan(), null, accountHandler.getThing().getUID());
final ThingUID accountUID = accountHandler.getThing().getUID();
accountHandler.updateModelFromServerAndUpdateThingStatus();
final SensiboModel model = accountHandler.getModel();
for (final SensiboSky pod : model.getPods()) {
final ThingUID podUID = new ThingUID(SensiboBindingConstants.THING_TYPE_SENSIBOSKY, accountUID,
String.valueOf(pod.getMacAddress()));
Map<String, String> properties = pod.getThingProperties();
// DiscoveryResult result uses Map<String,Object> as properties while ThingBuilder uses
// Map<String,String>
Map<String, Object> stringObjectProperties = new HashMap<>();
stringObjectProperties.putAll(properties);
final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(podUID).withBridge(accountUID)
.withLabel(pod.getProductName()).withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS)
.withProperties(stringObjectProperties).build();
thingDiscovered(discoveryResult);
}
}
}
@Override
protected void stopBackgroundDiscovery() {
stopScan();
discoveryJob.ifPresent(job -> {
if (!job.isCancelled()) {
job.cancel(true);
}
discoveryJob = Optional.empty();
});
}
@Override
protected void stopScan() {
logger.debug("Stop scan for Sensibo devices.");
super.stopScan();
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.sensibo.internal.dto;
import org.eclipse.jetty.http.HttpMethod;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public abstract class AbstractRequest {
public abstract String getRequestUrl();
public String getMethod() {
return HttpMethod.GET.asString();
}
}

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.sensibo.internal.dto.deletetimer;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class DeleteTimerReponse {
}

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.sensibo.internal.dto.deletetimer;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.sensibo.internal.dto.AbstractRequest;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class DeleteTimerRequest extends AbstractRequest {
public final transient String podId; // Transient fields are ignored by gson
public DeleteTimerRequest(String podId) {
this.podId = podId;
}
@Override
public String getRequestUrl() {
return String.format("/v1/pods/%s/timer/", podId);
}
@Override
public String getMethod() {
return HttpMethod.DELETE.asString();
}
}

View File

@@ -0,0 +1,50 @@
/**
* 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.sensibo.internal.dto.poddetails;
import org.openhab.binding.sensibo.internal.SensiboTemperatureUnitConverter;
import org.openhab.binding.sensibo.internal.model.AcState;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class AcStateDTO {
public boolean on;
public final String fanLevel;
public final String temperatureUnit;
public final Integer targetTemperature;
public final String mode;
public final String swing;
public AcStateDTO(boolean on, String fanLevel, String temperatureUnit, Integer targetTemperature, String mode,
String swing) {
this.on = on;
this.fanLevel = fanLevel;
this.temperatureUnit = temperatureUnit;
this.targetTemperature = targetTemperature;
this.mode = mode;
this.swing = swing;
}
public AcStateDTO(AcState acState) {
this.on = acState.isOn();
this.fanLevel = acState.getFanLevel();
this.targetTemperature = acState.getTargetTemperature();
this.mode = acState.getMode();
this.swing = acState.getSwing();
this.temperatureUnit = SensiboTemperatureUnitConverter.toSensiboFormat(acState.getTemperatureUnit());
}
}

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.sensibo.internal.dto.poddetails;
import com.google.gson.annotations.SerializedName;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class ConnectionStatusDTO {
@SerializedName("isAlive")
public boolean alive;
}

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.sensibo.internal.dto.poddetails;
import org.openhab.binding.sensibo.internal.dto.AbstractRequest;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class GetPodsDetailsRequest extends AbstractRequest {
public final String id;
public GetPodsDetailsRequest(final String id) {
this.id = id;
}
@Override
public String getRequestUrl() {
return String.format("/v2/pods/%s", id);
}
}

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.sensibo.internal.dto.poddetails;
import java.time.ZonedDateTime;
import com.google.gson.annotations.SerializedName;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class MeasurementDTO {
public Double batteryVoltage;
public Double temperature;
public Double humidity;
@SerializedName("rssi")
public Integer wifiSignalStrength;
@SerializedName("time")
public TimeWrapperDTO measurementTimestamp;
public ZonedDateTime getMeasurementTimestamp() {
if (measurementTimestamp != null) {
return measurementTimestamp.time;
}
return null;
}
}

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.sensibo.internal.dto.poddetails;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.gson.annotations.SerializedName;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class ModeCapabilityDTO {
@SerializedName("swing")
public List<String> swingModes = new ArrayList<>();
public Map<String, TemperatureDTO> temperatures = new HashMap<>();
public List<String> fanLevels = new ArrayList<>();
}

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.sensibo.internal.dto.poddetails;
import java.util.Map;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class ModeCapabilityWrapperDTO {
public Map<String, ModeCapabilityDTO> modes;
}

View File

@@ -0,0 +1,54 @@
/**
* 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.sensibo.internal.dto.poddetails;
import java.util.Map;
import com.google.gson.annotations.SerializedName;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class PodDetailsDTO {
public String id;
public String macAddress;
public String firmwareVersion;
public String firmwareType;
@SerializedName("serial")
public String serialNumber;
public String temperatureUnit;
public String productModel;
public AcStateDTO acState;
@SerializedName("measurements")
public MeasurementDTO lastMeasurement;
public ConnectionStatusDTO connectionStatus;
public RoomDTO room;
public ScheduleDTO[] schedules;
public TimerDTO timer;
private ModeCapabilityWrapperDTO remoteCapabilities;
public Map<String, ModeCapabilityDTO> getRemoteCapabilities() {
return remoteCapabilities.modes;
}
public boolean isAlive() {
return connectionStatus.alive;
}
public String getRoomName() {
return room.name;
}
}

View File

@@ -0,0 +1,23 @@
/**
* 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.sensibo.internal.dto.poddetails;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class RoomDTO {
public String name;
}

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.sensibo.internal.dto.poddetails;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class ScheduleDTO {
public String targetTimeLocal;
public String nextTime;
public String[] recurringDays;
public AcStateDTO acState;
public boolean enabled;
}

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.sensibo.internal.dto.poddetails;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class TemperatureDTO {
public boolean isNative;
@SerializedName("values")
public List<Integer> validValues = new ArrayList<>();
}

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.sensibo.internal.dto.poddetails;
import java.time.ZonedDateTime;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class TimeWrapperDTO {
public ZonedDateTime time;
}

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.sensibo.internal.dto.poddetails;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class TimerDTO {
public int targetTimeSecondsFromNow;
public AcStateDTO acState;
public boolean enabled;
}

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.sensibo.internal.dto.pods;
import org.openhab.binding.sensibo.internal.dto.AbstractRequest;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class GetPodsRequest extends AbstractRequest {
@Override
public String getRequestUrl() {
return "/v2/users/me/pods";
}
}

View File

@@ -0,0 +1,23 @@
/**
* 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.sensibo.internal.dto.pods;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class PodDTO {
public String id;
}

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.sensibo.internal.dto.setacstateproperty;
import org.openhab.binding.sensibo.internal.dto.poddetails.AcStateDTO;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class SetAcStatePropertyReponse {
public AcStateDTO acState;
}

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.sensibo.internal.dto.setacstateproperty;
import org.openhab.binding.sensibo.internal.dto.AbstractRequest;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class SetAcStatePropertyRequest extends AbstractRequest {
public transient String podId; // Transient fields are ignored by gson
public transient String property;
public Object newValue;
public SetAcStatePropertyRequest(String podId, String property, Object value) {
this.podId = podId;
this.property = property;
this.newValue = value;
}
@Override
public String getRequestUrl() {
return String.format("/v2/pods/%s/acStates/%s", podId, property);
}
@Override
public String getMethod() {
return "PATCH";
}
}

View File

@@ -0,0 +1,23 @@
/**
* 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.sensibo.internal.dto.settimer;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
public class SetTimerReponse {
}

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.sensibo.internal.dto.settimer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.sensibo.internal.dto.AbstractRequest;
import org.openhab.binding.sensibo.internal.dto.poddetails.AcStateDTO;
/**
* All classes in the ..binding.sensibo.dto are data transfer classes used by the GSON mapper. This class reflects a
* part of a request/response data structure.
*
* @author Arne Seime - Initial contribution.
*/
@NonNullByDefault
public class SetTimerRequest extends AbstractRequest {
public final transient String podId; // Transient fields are ignored by gson
public final AcStateDTO acState;
public final int minutesFromNow;
public SetTimerRequest(String podId, int minutesFromNow, AcStateDTO acState) {
this.podId = podId;
this.acState = acState;
this.minutesFromNow = minutesFromNow;
}
@Override
public String getRequestUrl() {
return String.format("/v1/pods/%s/timer/", podId);
}
@Override
public String getMethod() {
return HttpMethod.PUT.asString();
}
}

View File

@@ -0,0 +1,334 @@
/**
* 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.sensibo.internal.handler;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
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.sensibo.internal.SensiboCommunicationException;
import org.openhab.binding.sensibo.internal.SensiboConfigurationException;
import org.openhab.binding.sensibo.internal.SensiboException;
import org.openhab.binding.sensibo.internal.client.RequestLogger;
import org.openhab.binding.sensibo.internal.config.SensiboAccountConfiguration;
import org.openhab.binding.sensibo.internal.dto.AbstractRequest;
import org.openhab.binding.sensibo.internal.dto.deletetimer.DeleteTimerReponse;
import org.openhab.binding.sensibo.internal.dto.deletetimer.DeleteTimerRequest;
import org.openhab.binding.sensibo.internal.dto.poddetails.AcStateDTO;
import org.openhab.binding.sensibo.internal.dto.poddetails.GetPodsDetailsRequest;
import org.openhab.binding.sensibo.internal.dto.poddetails.PodDetailsDTO;
import org.openhab.binding.sensibo.internal.dto.pods.GetPodsRequest;
import org.openhab.binding.sensibo.internal.dto.pods.PodDTO;
import org.openhab.binding.sensibo.internal.dto.setacstateproperty.SetAcStatePropertyReponse;
import org.openhab.binding.sensibo.internal.dto.setacstateproperty.SetAcStatePropertyRequest;
import org.openhab.binding.sensibo.internal.dto.settimer.SetTimerReponse;
import org.openhab.binding.sensibo.internal.dto.settimer.SetTimerRequest;
import org.openhab.binding.sensibo.internal.model.AcState;
import org.openhab.binding.sensibo.internal.model.SensiboModel;
import org.openhab.binding.sensibo.internal.model.SensiboSky;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* The {@link SensiboAccountHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class SensiboAccountHandler extends BaseBridgeHandler {
private static final int MIN_TIME_BETWEEEN_MODEL_UPDATES_MS = 30_000;
private static final int SECONDS_IN_MINUTE = 60;
public static String API_ENDPOINT = "https://home.sensibo.com/api";
private final Logger logger = LoggerFactory.getLogger(SensiboAccountHandler.class);
private final HttpClient httpClient;
private final JsonParser jsonParser = new JsonParser();
private final RequestLogger requestLogger;
private final Gson gson;
private SensiboModel model = new SensiboModel(0);
private Optional<ScheduledFuture<?>> statusFuture = Optional.empty();
private @NonNullByDefault({}) SensiboAccountConfiguration config;
public SensiboAccountHandler(final Bridge bridge, final HttpClient httpClient) {
super(bridge);
this.httpClient = httpClient;
gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, new TypeAdapter<ZonedDateTime>() {
@Override
public void write(final @NonNullByDefault({}) JsonWriter out, final ZonedDateTime value)
throws IOException {
out.value(value.toString());
}
@Override
public ZonedDateTime read(final @NonNullByDefault({}) JsonReader in) throws IOException {
return ZonedDateTime.parse(in.nextString());
}
}).setLenient().setPrettyPrinting().create();
requestLogger = new RequestLogger(bridge.getUID().getId(), gson);
}
private boolean allowModelUpdate() {
final long diffMsSinceLastUpdate = System.currentTimeMillis() - model.getLastUpdated();
return diffMsSinceLastUpdate > MIN_TIME_BETWEEEN_MODEL_UPDATES_MS;
}
public SensiboModel getModel() {
return model;
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
// Ignore commands as none are supported
}
public SensiboAccountConfiguration loadConfigSafely() throws SensiboConfigurationException {
SensiboAccountConfiguration loadedConfig = getConfigAs(SensiboAccountConfiguration.class);
if (loadedConfig == null) {
throw new SensiboConfigurationException("Could not load Sensibo account configuration");
}
return loadedConfig;
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
scheduler.execute(this::initializeInternal);
}
private void initializeInternal() {
try {
config = loadConfigSafely();
logger.debug("Initializing Sensibo Account bridge using config {}", config);
model = refreshModel();
updateStatus(ThingStatus.ONLINE);
initPolling();
logger.debug("Initialization of Sensibo account completed successfully for {}", config);
} catch (final SensiboConfigurationException e) {
logger.info("Error initializing Sensibo data: {}", e.getMessage());
model = new SensiboModel(0); // Empty model
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Error fetching initial data: " + e.getMessage());
} catch (final SensiboException e) {
logger.info("Error initializing Sensibo data: {}", e.getMessage());
model = new SensiboModel(0); // Empty model
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Error fetching initial data: " + e.getMessage());
// Reschedule init
scheduler.schedule(this::initializeInternal, 30, TimeUnit.SECONDS);
}
}
@Override
public void dispose() {
stopPolling();
super.dispose();
}
/**
* starts this things polling future
*/
private void initPolling() {
stopPolling();
statusFuture = Optional.of(scheduler.scheduleWithFixedDelay(this::updateModelFromServerAndUpdateThingStatus,
config.refreshInterval, config.refreshInterval, TimeUnit.SECONDS));
}
protected SensiboModel refreshModel() throws SensiboException {
final SensiboModel updatedModel = new SensiboModel(System.currentTimeMillis());
final GetPodsRequest getPodsRequest = new GetPodsRequest();
final List<PodDTO> pods = sendRequest(buildRequest(getPodsRequest), getPodsRequest,
new TypeToken<ArrayList<PodDTO>>() {
}.getType());
for (final PodDTO pod : pods) {
final GetPodsDetailsRequest getPodsDetailsRequest = new GetPodsDetailsRequest(pod.id);
final PodDetailsDTO podDetails = sendRequest(buildGetPodDetailsRequest(getPodsDetailsRequest),
getPodsDetailsRequest, new TypeToken<PodDetailsDTO>() {
}.getType());
updatedModel.addPod(new SensiboSky(podDetails));
}
return updatedModel;
}
private <T> T sendRequest(final Request request, final AbstractRequest req, final Type responseType)
throws SensiboException {
try {
final ContentResponse contentResponse = request.send();
final String responseJson = contentResponse.getContentAsString();
if (contentResponse.getStatus() == HttpStatus.OK_200) {
final JsonObject o = jsonParser.parse(responseJson).getAsJsonObject();
final String overallStatus = o.get("status").getAsString();
if ("success".equals(overallStatus)) {
return gson.fromJson(o.get("result"), responseType);
} else {
throw new SensiboCommunicationException(req, overallStatus);
}
} else if (contentResponse.getStatus() == HttpStatus.FORBIDDEN_403) {
throw new SensiboConfigurationException("Invalid API key");
} else {
throw new SensiboCommunicationException(
"Error sending request to Sensibo server. Server responded with " + contentResponse.getStatus()
+ " and payload " + responseJson);
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new SensiboCommunicationException(
String.format("Error sending request to Sensibo server: %s", e.getMessage()), e);
}
}
/**
* Stops this thing's polling future
*/
private void stopPolling() {
statusFuture.ifPresent(future -> {
if (!future.isCancelled()) {
future.cancel(true);
}
statusFuture = Optional.empty();
});
}
public void updateModelFromServerAndUpdateThingStatus() {
if (allowModelUpdate()) {
try {
model = refreshModel();
updateThingStatuses();
updateStatus(ThingStatus.ONLINE);
} catch (SensiboConfigurationException e) {
logger.debug("Error updating Sensibo model do to {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
} catch (SensiboException e) {
logger.debug("Error updating Sensibo model do to {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
}
private void updateThingStatuses() {
final List<Thing> subThings = getThing().getThings();
for (final Thing thing : subThings) {
final ThingHandler handler = thing.getHandler();
if (handler != null) {
final SensiboBaseThingHandler mHandler = (SensiboBaseThingHandler) handler;
mHandler.updateState(model);
}
}
}
private Request buildGetPodDetailsRequest(final GetPodsDetailsRequest getPodsDetailsRequest) {
final Request req = buildRequest(getPodsDetailsRequest);
req.param("fields", "*");
return req;
}
private Request buildRequest(final AbstractRequest req) {
Request request = httpClient.newRequest(API_ENDPOINT + req.getRequestUrl()).param("apiKey", config.apiKey)
.method(req.getMethod());
if (!req.getMethod().contentEquals(HttpMethod.GET.asString())) { // POST, PATCH
final String reqJson = gson.toJson(req);
request = request.content(new BytesContentProvider(reqJson.getBytes(StandardCharsets.UTF_8)),
"application/json");
}
requestLogger.listenTo(request, new String[] { config.apiKey });
return request;
}
public void updateSensiboSkyAcState(final String macAddress, String property, Object value,
SensiboBaseThingHandler handler) {
model.findSensiboSkyByMacAddress(macAddress).ifPresent(pod -> {
try {
SetAcStatePropertyRequest setAcStatePropertyRequest = new SetAcStatePropertyRequest(pod.getId(),
property, value);
Request request = buildRequest(setAcStatePropertyRequest);
SetAcStatePropertyReponse response = sendRequest(request, setAcStatePropertyRequest,
new TypeToken<SetAcStatePropertyReponse>() {
}.getType());
model.updateAcState(macAddress, new AcState(response.acState));
handler.updateState(model);
} catch (SensiboException e) {
logger.debug("Error setting ac state for {}", macAddress, e);
}
});
}
public void updateSensiboSkyTimer(final String macAddress, @Nullable Integer secondsFromNow) {
model.findSensiboSkyByMacAddress(macAddress).ifPresent(pod -> {
try {
if (secondsFromNow != null && secondsFromNow >= SECONDS_IN_MINUTE) {
AcStateDTO offState = new AcStateDTO(pod.getAcState().get());
offState.on = false;
SetTimerRequest setTimerRequest = new SetTimerRequest(pod.getId(),
secondsFromNow / SECONDS_IN_MINUTE, offState);
Request request = buildRequest(setTimerRequest);
// No data in response
sendRequest(request, setTimerRequest, new TypeToken<SetTimerReponse>() {
}.getType());
} else {
DeleteTimerRequest setTimerRequest = new DeleteTimerRequest(pod.getId());
Request request = buildRequest(setTimerRequest);
// No data in response
sendRequest(request, setTimerRequest, new TypeToken<DeleteTimerReponse>() {
}.getType());
}
} catch (SensiboException e) {
logger.debug("Error setting timer for {}", macAddress, e);
}
});
}
}

View File

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

View File

@@ -0,0 +1,503 @@
/**
* 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.sensibo.internal.handler;
import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_CURRENT_HUMIDITY;
import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_CURRENT_TEMPERATURE;
import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_FAN_LEVEL;
import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_MASTER_SWITCH;
import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_MODE;
import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_SWING_MODE;
import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_TARGET_TEMPERATURE;
import static org.openhab.binding.sensibo.internal.SensiboBindingConstants.CHANNEL_TIMER;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.measure.IncommensurableException;
import javax.measure.UnconvertibleException;
import javax.measure.Unit;
import javax.measure.UnitConverter;
import javax.measure.quantity.Temperature;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.sensibo.internal.CallbackChannelsTypeProvider;
import org.openhab.binding.sensibo.internal.SensiboBindingConstants;
import org.openhab.binding.sensibo.internal.config.SensiboSkyConfiguration;
import org.openhab.binding.sensibo.internal.dto.poddetails.TemperatureDTO;
import org.openhab.binding.sensibo.internal.model.SensiboModel;
import org.openhab.binding.sensibo.internal.model.SensiboSky;
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.types.StringType;
import org.openhab.core.library.unit.SIUnits;
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.ThingHandlerService;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.thing.type.StateChannelTypeBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tec.uom.se.unit.Units;
/**
* The {@link SensiboSkyHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class SensiboSkyHandler extends SensiboBaseThingHandler implements ChannelTypeProvider {
public static final String SWING_PROPERTY = "swing";
public static final String MASTER_SWITCH_PROPERTY = "on";
public static final String FAN_LEVEL_PROPERTY = "fanLevel";
public static final String MODE_PROPERTY = "mode";
public static final String TARGET_TEMPERATURE_PROPERTY = "targetTemperature";
public static final String SWING_MODE_LABEL = "Swing Mode";
public static final String FAN_LEVEL_LABEL = "Fan Level";
public static final String MODE_LABEL = "Mode";
public static final String TARGET_TEMPERATURE_LABEL = "Target Temperature";
private static final String ITEM_TYPE_STRING = "String";
private static final String ITEM_TYPE_NUMBER_TEMPERATURE = "Number:Temperature";
private final Logger logger = LoggerFactory.getLogger(SensiboSkyHandler.class);
private final Map<ChannelTypeUID, ChannelType> generatedChannelTypes = new HashMap<>();
private Optional<SensiboSkyConfiguration> config = Optional.empty();
public SensiboSkyHandler(final Thing thing) {
super(thing);
}
private static String beautify(final String camelCaseWording) {
final StringBuilder b = new StringBuilder();
for (final String s : StringUtils.splitByCharacterTypeCamelCase(camelCaseWording)) {
b.append(" ");
b.append(s);
}
final StringBuilder bs = new StringBuilder();
for (final String t : StringUtils.splitByWholeSeparator(b.toString(), " _")) {
bs.append(" ");
bs.append(t);
}
return WordUtils.capitalizeFully(bs.toString()).trim();
}
private String getMacAddress() {
if (config.isPresent()) {
return config.get().macAddress;
}
throw new IllegalArgumentException("No configuration present");
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
handleCommand(channelUID, command, getSensiboModel());
}
/*
* Package private in order to be reachable from unit test
*/
void updateAcState(SensiboSky sensiboSky, String property, Object value) {
StateChange stateChange = checkStateChangeValid(sensiboSky, property, value);
if (stateChange.valid) {
getAccountHandler().ifPresent(
handler -> handler.updateSensiboSkyAcState(getMacAddress(), property, stateChange.value, this));
} else {
logger.info("Update command not sent; invalid state change for SensiboSky AC state: {}",
stateChange.validationMessage);
}
}
private void updateTimer(@Nullable Integer secondsFromNowUntilSwitchOff) {
getAccountHandler()
.ifPresent(handler -> handler.updateSensiboSkyTimer(getMacAddress(), secondsFromNowUntilSwitchOff));
}
@Override
protected void handleCommand(final ChannelUID channelUID, final Command command, final SensiboModel model) {
model.findSensiboSkyByMacAddress(getMacAddress()).ifPresent(sensiboSky -> {
if (sensiboSky.isAlive()) {
if (getThing().getStatus() != ThingStatus.ONLINE) {
addDynamicChannelsAndProperties(sensiboSky);
updateStatus(ThingStatus.ONLINE); // In case it has been offline
}
switch (channelUID.getId()) {
case CHANNEL_CURRENT_HUMIDITY:
handleCurrentHumidityCommand(channelUID, command, sensiboSky);
break;
case CHANNEL_CURRENT_TEMPERATURE:
handleCurrentTemperatureCommand(channelUID, command, sensiboSky);
break;
case CHANNEL_MASTER_SWITCH:
handleMasterSwitchCommand(channelUID, command, sensiboSky);
break;
case CHANNEL_TARGET_TEMPERATURE:
handleTargetTemperatureCommand(channelUID, command, sensiboSky);
break;
case CHANNEL_MODE:
handleModeCommand(channelUID, command, sensiboSky);
break;
case CHANNEL_SWING_MODE:
handleSwingCommand(channelUID, command, sensiboSky);
break;
case CHANNEL_FAN_LEVEL:
handleFanLevelCommand(channelUID, command, sensiboSky);
break;
case CHANNEL_TIMER:
handleTimerCommand(channelUID, command, sensiboSky);
break;
default:
logger.debug("Received command on unknown channel {}, ignoring", channelUID.getId());
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Unreachable by Sensibo servers");
}
});
}
private void handleTimerCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
if (command instanceof RefreshType) {
if (sensiboSky.getTimer().isPresent() && sensiboSky.getTimer().get().secondsRemaining > 0) {
updateState(channelUID, new DecimalType(sensiboSky.getTimer().get().secondsRemaining));
} else {
updateState(channelUID, UnDefType.UNDEF);
}
} else if (command instanceof DecimalType) {
final DecimalType newValue = (DecimalType) command;
updateTimer(newValue.intValue());
} else {
updateTimer(null);
}
}
private void handleFanLevelCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
if (command instanceof RefreshType) {
if (sensiboSky.getAcState().isPresent() && sensiboSky.getAcState().get().getFanLevel() != null) {
updateState(channelUID, new StringType(sensiboSky.getAcState().get().getFanLevel()));
} else {
updateState(channelUID, UnDefType.UNDEF);
}
} else if (command instanceof StringType) {
final StringType newValue = (StringType) command;
updateAcState(sensiboSky, FAN_LEVEL_PROPERTY, newValue.toString());
}
}
private void handleSwingCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
if (command instanceof RefreshType && sensiboSky.getAcState().isPresent()) {
if (sensiboSky.getAcState().isPresent() && sensiboSky.getAcState().get().getSwing() != null) {
updateState(channelUID, new StringType(sensiboSky.getAcState().get().getSwing()));
} else {
updateState(channelUID, UnDefType.UNDEF);
}
} else if (command instanceof StringType) {
final StringType newValue = (StringType) command;
updateAcState(sensiboSky, SWING_PROPERTY, newValue.toString());
}
}
private void handleModeCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
if (command instanceof RefreshType) {
if (sensiboSky.getAcState().isPresent()) {
updateState(channelUID, new StringType(sensiboSky.getAcState().get().getMode()));
} else {
updateState(channelUID, UnDefType.UNDEF);
}
} else if (command instanceof StringType) {
final StringType newValue = (StringType) command;
updateAcState(sensiboSky, MODE_PROPERTY, newValue.toString());
addDynamicChannelsAndProperties(sensiboSky);
}
}
private void handleTargetTemperatureCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
if (command instanceof RefreshType) {
sensiboSky.getAcState().ifPresent(acState -> {
@Nullable
Integer targetTemperature = acState.getTargetTemperature();
if (targetTemperature != null) {
updateState(channelUID, new QuantityType<>(targetTemperature, sensiboSky.getTemperatureUnit()));
} else {
updateState(channelUID, UnDefType.UNDEF);
}
});
if (!sensiboSky.getAcState().isPresent()) {
updateState(channelUID, UnDefType.UNDEF);
}
} else if (command instanceof QuantityType<?>) {
QuantityType<?> newValue = (QuantityType<?>) command;
if (!Objects.equals(sensiboSky.getTemperatureUnit(), newValue.getUnit())) {
// If quantity is given in celsius when fahrenheit is used or opposite
try {
UnitConverter temperatureConverter = newValue.getUnit()
.getConverterToAny(sensiboSky.getTemperatureUnit());
// No decimals supported
long convertedValue = (long) temperatureConverter.convert(newValue.longValue());
updateAcState(sensiboSky, TARGET_TEMPERATURE_PROPERTY, new DecimalType(convertedValue));
} catch (UnconvertibleException | IncommensurableException e) {
logger.info("Could not convert {} to {}: {}", newValue, sensiboSky.getTemperatureUnit(),
e.getMessage());
}
} else {
updateAcState(sensiboSky, TARGET_TEMPERATURE_PROPERTY, new DecimalType(newValue.intValue()));
}
} else if (command instanceof DecimalType) {
updateAcState(sensiboSky, TARGET_TEMPERATURE_PROPERTY, command);
}
}
private void handleMasterSwitchCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
if (command instanceof RefreshType) {
sensiboSky.getAcState().ifPresent(e -> updateState(channelUID, OnOffType.from(e.isOn())));
} else if (command instanceof OnOffType) {
updateAcState(sensiboSky, MASTER_SWITCH_PROPERTY, command == OnOffType.ON);
}
}
private void handleCurrentTemperatureCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
if (command instanceof RefreshType) {
updateState(channelUID, new QuantityType<>(sensiboSky.getTemperature(), SIUnits.CELSIUS));
}
}
private void handleCurrentHumidityCommand(ChannelUID channelUID, Command command, SensiboSky sensiboSky) {
if (command instanceof RefreshType) {
updateState(channelUID, new QuantityType<>(sensiboSky.getHumidity(), Units.PERCENT));
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(CallbackChannelsTypeProvider.class);
}
@Override
public void initialize() {
config = Optional.ofNullable(getConfigAs(SensiboSkyConfiguration.class));
logger.debug("Initializing SensiboSky using config {}", config);
getSensiboModel().findSensiboSkyByMacAddress(getMacAddress()).ifPresent(pod -> {
if (pod.isAlive()) {
addDynamicChannelsAndProperties(pod);
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Unreachable by Sensibo servers");
}
});
}
private boolean isDynamicChannel(final ChannelTypeUID uid) {
return SensiboBindingConstants.DYNAMIC_CHANNEL_TYPES.stream().anyMatch(e -> uid.getId().startsWith(e));
}
private void addDynamicChannelsAndProperties(final SensiboSky sensiboSky) {
logger.debug("Updating dynamic channels for {}", sensiboSky.getId());
final List<Channel> newChannels = new ArrayList<>();
for (final Channel channel : getThing().getChannels()) {
final ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
if (channelTypeUID != null && !isDynamicChannel(channelTypeUID)) {
newChannels.add(channel);
}
}
newChannels.addAll(createDynamicChannels(sensiboSky));
Map<String, String> properties = sensiboSky.getThingProperties();
updateThing(editThing().withChannels(newChannels).withProperties(properties).build());
}
public List<Channel> createDynamicChannels(final SensiboSky sensiboSky) {
final List<Channel> newChannels = new ArrayList<>();
generatedChannelTypes.clear();
sensiboSky.getCurrentModeCapabilities().ifPresent(capabilities -> {
// Not all modes have swing and fan level
final ChannelTypeUID swingModeChannelType = addChannelType(SensiboBindingConstants.CHANNEL_TYPE_SWING_MODE,
SWING_MODE_LABEL, ITEM_TYPE_STRING, capabilities.swingModes, null, null);
newChannels
.add(ChannelBuilder
.create(new ChannelUID(getThing().getUID(), SensiboBindingConstants.CHANNEL_SWING_MODE),
ITEM_TYPE_STRING)
.withLabel(SWING_MODE_LABEL).withType(swingModeChannelType).build());
final ChannelTypeUID fanLevelChannelType = addChannelType(SensiboBindingConstants.CHANNEL_TYPE_FAN_LEVEL,
FAN_LEVEL_LABEL, ITEM_TYPE_STRING, capabilities.fanLevels, null, null);
newChannels.add(ChannelBuilder
.create(new ChannelUID(getThing().getUID(), SensiboBindingConstants.CHANNEL_FAN_LEVEL),
ITEM_TYPE_STRING)
.withLabel(FAN_LEVEL_LABEL).withType(fanLevelChannelType).build());
});
final ChannelTypeUID modeChannelType = addChannelType(SensiboBindingConstants.CHANNEL_TYPE_MODE, MODE_LABEL,
ITEM_TYPE_STRING, sensiboSky.getRemoteCapabilities().keySet(), null, null);
newChannels.add(ChannelBuilder
.create(new ChannelUID(getThing().getUID(), SensiboBindingConstants.CHANNEL_MODE), ITEM_TYPE_STRING)
.withLabel(MODE_LABEL).withType(modeChannelType).build());
final ChannelTypeUID targetTemperatureChannelType = addChannelType(
SensiboBindingConstants.CHANNEL_TYPE_TARGET_TEMPERATURE, TARGET_TEMPERATURE_LABEL,
ITEM_TYPE_NUMBER_TEMPERATURE, sensiboSky.getTargetTemperatures(), "%d %unit%", "TargetTemperature");
newChannels.add(ChannelBuilder
.create(new ChannelUID(getThing().getUID(), SensiboBindingConstants.CHANNEL_TARGET_TEMPERATURE),
ITEM_TYPE_NUMBER_TEMPERATURE)
.withLabel(TARGET_TEMPERATURE_LABEL).withType(targetTemperatureChannelType).build());
return newChannels;
}
private ChannelTypeUID addChannelType(final String channelTypePrefix, final String label, final String itemType,
final Collection<?> options, @Nullable final String pattern, @Nullable final String tag) {
final ChannelTypeUID channelTypeUID = new ChannelTypeUID(SensiboBindingConstants.BINDING_ID,
channelTypePrefix + getThing().getUID().getId());
final List<StateOption> stateOptions = options.stream()
.map(e -> new StateOption(e.toString(), e instanceof String ? beautify((String) e) : e.toString()))
.collect(Collectors.toList());
StateDescriptionFragmentBuilder stateDescription = StateDescriptionFragmentBuilder.create().withReadOnly(false)
.withOptions(stateOptions);
if (pattern != null) {
stateDescription = stateDescription.withPattern(pattern);
}
final StateChannelTypeBuilder builder = ChannelTypeBuilder.state(channelTypeUID, label, itemType)
.withStateDescription(stateDescription.build().toStateDescription());
if (tag != null) {
builder.withTag(tag);
}
final ChannelType channelType = builder.build();
generatedChannelTypes.put(channelTypeUID, channelType);
return channelTypeUID;
}
@Override
public Collection<ChannelType> getChannelTypes(@Nullable final Locale locale) {
return generatedChannelTypes.values();
}
@Override
public @Nullable ChannelType getChannelType(final ChannelTypeUID channelTypeUID, @Nullable final Locale locale) {
return generatedChannelTypes.get(channelTypeUID);
}
/*
* Package private in order to be reachable from unit test
*/
StateChange checkStateChangeValid(SensiboSky sensiboSky, String property, Object newPropertyValue) {
StateChange stateChange = new StateChange(newPropertyValue);
sensiboSky.getCurrentModeCapabilities().ifPresent(currentModeCapabilities -> {
switch (property) {
case TARGET_TEMPERATURE_PROPERTY:
Unit<Temperature> temperatureUnit = sensiboSky.getTemperatureUnit();
TemperatureDTO validTemperatures = currentModeCapabilities.temperatures
.get(temperatureUnit == SIUnits.CELSIUS ? "C" : "F");
DecimalType rawValue = (DecimalType) newPropertyValue;
stateChange.updateValue(rawValue.intValue());
if (!validTemperatures.validValues.contains(rawValue.intValue())) {
stateChange.addError(String.format(
"Cannot change targetTemperature to '%d', valid targetTemperatures are one of %s",
rawValue.intValue(), ToStringBuilder.reflectionToString(
validTemperatures.validValues.toArray(), ToStringStyle.SIMPLE_STYLE)));
}
break;
case MODE_PROPERTY:
if (!sensiboSky.getRemoteCapabilities().containsKey(newPropertyValue)) {
stateChange.addError(
String.format("Cannot change mode to %s, valid modes are %s", newPropertyValue,
ToStringBuilder.reflectionToString(
sensiboSky.getRemoteCapabilities().keySet().toArray(),
ToStringStyle.SIMPLE_STYLE)));
}
break;
case FAN_LEVEL_PROPERTY:
if (!currentModeCapabilities.fanLevels.contains(newPropertyValue)) {
stateChange.addError(String.format("Cannot change fanLevel to %s, valid fanLevels are %s",
newPropertyValue, ToStringBuilder.reflectionToString(
currentModeCapabilities.fanLevels.toArray(), ToStringStyle.SIMPLE_STYLE)));
}
break;
case MASTER_SWITCH_PROPERTY:
// Always allowed
break;
case SWING_PROPERTY:
if (!currentModeCapabilities.swingModes.contains(newPropertyValue)) {
stateChange.addError(String.format("Cannot change swing to %s, valid swings are %s",
newPropertyValue, ToStringBuilder.reflectionToString(
currentModeCapabilities.swingModes.toArray(), ToStringStyle.SIMPLE_STYLE)));
}
break;
default:
stateChange.addError(String.format("No such ac state property %s", property));
}
logger.debug("State change request {}", stateChange);
});
return stateChange;
}
@NonNullByDefault
public class StateChange {
Object value;
boolean valid = true;
@Nullable
String validationMessage;
public StateChange(Object value) {
this.value = value;
}
public void updateValue(Object updatedValue) {
value = updatedValue;
}
public void addError(String validationMessage) {
valid = false;
this.validationMessage = validationMessage;
}
@Override
public String toString() {
return "StateChange [valid=" + valid + ", validationMessage=" + validationMessage + ", value=" + value
+ ", value Class=" + value.getClass() + "]";
}
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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.sensibo.internal.model;
import javax.measure.Unit;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.sensibo.internal.SensiboTemperatureUnitConverter;
import org.openhab.binding.sensibo.internal.dto.poddetails.AcStateDTO;
/**
* Represents the state of the AC unit.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class AcState {
private final boolean on;
private final @Nullable String fanLevel;
private final @Nullable Unit<Temperature> temperatureUnit;
private final @Nullable Integer targetTemperature;
private final @Nullable String mode;
private final @Nullable String swing;
public AcState(final AcStateDTO dto) {
this.on = dto.on;
this.fanLevel = dto.fanLevel;
this.targetTemperature = dto.targetTemperature;
this.mode = dto.mode;
this.swing = dto.swing;
this.temperatureUnit = SensiboTemperatureUnitConverter.parseFromSensiboFormat(dto.temperatureUnit);
}
public boolean isOn() {
return on;
}
@Nullable
public String getFanLevel() {
return fanLevel;
}
@Nullable
public Unit<Temperature> getTemperatureUnit() {
return temperatureUnit;
}
@Nullable
public Integer getTargetTemperature() {
return targetTemperature;
}
@Nullable
public String getMode() {
return mode;
}
@Nullable
public String getSwing() {
return swing;
}
}

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.sensibo.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents a generic Sensibo controllable thing
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public abstract class Pod {
protected String id;
public String getId() {
return id;
}
protected Pod(String id) {
this.id = id;
}
}

View File

@@ -0,0 +1,64 @@
/**
* 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.sensibo.internal.model;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.sensibo.internal.dto.poddetails.ScheduleDTO;
/**
* The {@link SensiboSky} represents a Sensibo Sky schedule
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class Schedule {
private final LocalTime targetTimeLocal;
private final String[] recurringDays;
private final AcState acState;
private final boolean enabled;
private @Nullable ZonedDateTime nextTime;
public Schedule(ScheduleDTO dto) {
this.enabled = dto.enabled;
if (enabled) {
this.nextTime = ZonedDateTime.parse(nextTime + "Z"); // API field seems to be in Zulu
}
this.targetTimeLocal = LocalTime.parse(dto.targetTimeLocal);
this.recurringDays = dto.recurringDays;
this.acState = new AcState(dto.acState);
}
public LocalTime getTargetTimeLocal() {
return targetTimeLocal;
}
public @Nullable ZonedDateTime getNextTime() {
return nextTime;
}
public String[] getRecurringDays() {
return recurringDays;
}
public AcState getAcState() {
return acState;
}
public boolean isEnabled() {
return enabled;
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.sensibo.internal.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SensiboModel} represents the home structure as designed by the user in the Sensibo app.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class SensiboModel {
private final long lastUpdated;
private final List<SensiboSky> pods = new ArrayList<>();
public SensiboModel(final long lastUpdated) {
this.lastUpdated = lastUpdated;
}
public void addPod(final SensiboSky pod) {
pods.add(pod);
}
public List<SensiboSky> getPods() {
return pods;
}
public long getLastUpdated() {
return lastUpdated;
}
public Optional<SensiboSky> findSensiboSkyByMacAddress(final String macAddress) {
final String macAddressWithoutColons = StringUtils.remove(macAddress, ':');
return pods.stream().filter(pod -> macAddressWithoutColons.equals(pod.getMacAddress())).findFirst();
}
/**
* @param macAddress
* @param acState
*/
public void updateAcState(String macAddress, AcState acState) {
findSensiboSkyByMacAddress(macAddress).ifPresent(sky -> sky.updateAcState(acState));
}
}

View File

@@ -0,0 +1,198 @@
/**
* 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.sensibo.internal.model;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.measure.Unit;
import javax.measure.quantity.Temperature;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.sensibo.internal.SensiboTemperatureUnitConverter;
import org.openhab.binding.sensibo.internal.dto.poddetails.ModeCapabilityDTO;
import org.openhab.binding.sensibo.internal.dto.poddetails.PodDetailsDTO;
import org.openhab.binding.sensibo.internal.dto.poddetails.TemperatureDTO;
import org.openhab.core.thing.Thing;
/**
* The {@link SensiboSky} represents a Sensibo Sky unit
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class SensiboSky extends Pod {
private final String macAddress;
private final String firmwareVersion;
private final String firmwareType;
private final String serialNumber;
private final String productModel;
private final String roomName;
private final Unit<Temperature> temperatureUnit;
private final String originalTemperatureUnit;
private final Double temperature;
private final Double humidity;
private final boolean alive;
private final Map<String, ModeCapabilityDTO> remoteCapabilities;
private Schedule[] schedules = new Schedule[0];
private Optional<AcState> acState = Optional.empty();
private Optional<Timer> timer = Optional.empty();
public SensiboSky(final PodDetailsDTO dto) {
super(dto.id);
this.macAddress = StringUtils.remove(dto.macAddress, ':');
this.firmwareVersion = dto.firmwareVersion;
this.firmwareType = dto.firmwareType;
this.serialNumber = dto.serialNumber;
this.originalTemperatureUnit = dto.temperatureUnit;
this.temperatureUnit = SensiboTemperatureUnitConverter.parseFromSensiboFormat(dto.temperatureUnit);
this.productModel = dto.productModel;
if (dto.acState != null) {
this.acState = Optional.of(new AcState(dto.acState));
}
if (dto.timer != null) {
this.timer = Optional.of(new Timer(dto.timer));
}
this.temperature = dto.lastMeasurement.temperature;
this.humidity = dto.lastMeasurement.humidity;
this.alive = dto.isAlive();
if (dto.getRemoteCapabilities() != null) {
this.remoteCapabilities = dto.getRemoteCapabilities();
} else {
this.remoteCapabilities = new HashMap<>();
}
this.roomName = dto.getRoomName();
if (dto.schedules != null) {
schedules = Arrays.stream(dto.schedules).map(Schedule::new).toArray(Schedule[]::new);
}
}
public String getOriginalTemperatureUnit() {
return originalTemperatureUnit;
}
public String getRoomName() {
return roomName;
}
public Schedule[] getSchedules() {
return schedules;
}
public String getMacAddress() {
return macAddress;
}
public String getFirmwareVersion() {
return firmwareVersion;
}
public String getFirmwareType() {
return firmwareType;
}
public String getSerialNumber() {
return serialNumber;
}
public Unit<Temperature> getTemperatureUnit() {
return temperatureUnit;
}
public String getProductModel() {
return productModel;
}
public Optional<AcState> getAcState() {
return acState;
}
public String getProductName() {
switch (productModel) {
case "skyv2":
return String.format("Sensibo Sky %s", roomName);
default:
return String.format("%s %s", productModel, roomName);
}
}
public Double getTemperature() {
return temperature;
}
public Double getHumidity() {
return humidity;
}
public boolean isAlive() {
return alive;
}
public Map<String, ModeCapabilityDTO> getRemoteCapabilities() {
return remoteCapabilities;
}
public Optional<ModeCapabilityDTO> getCurrentModeCapabilities() {
if (acState.isPresent() && acState.get().getMode() != null) {
return Optional.ofNullable(remoteCapabilities.get(acState.get().getMode()));
} else {
return Optional.empty();
}
}
public List<Integer> getTargetTemperatures() {
Optional<ModeCapabilityDTO> currentModeCapabilities = getCurrentModeCapabilities();
if (currentModeCapabilities.isPresent()) {
TemperatureDTO selectedTemperatureRange = currentModeCapabilities.get().temperatures
.get(originalTemperatureUnit);
if (selectedTemperatureRange != null) {
return selectedTemperatureRange.validValues;
}
}
return Collections.emptyList();
}
/**
* @param newAcState an updated ac state
*/
public void updateAcState(AcState newAcState) {
this.acState = Optional.of(newAcState);
}
public Optional<Timer> getTimer() {
return timer;
}
public Map<String, String> getThingProperties() {
final Map<String, String> properties = new HashMap<>();
properties.put(Thing.PROPERTY_VENDOR, "Sensibo");
properties.put("podId", id);
properties.put(Thing.PROPERTY_MAC_ADDRESS, macAddress);
properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
properties.put(Thing.PROPERTY_MODEL_ID, productModel);
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
properties.put("firmwareType", firmwareType);
return properties;
}
}

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.sensibo.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.sensibo.internal.dto.poddetails.TimerDTO;
/**
* The {@link Timer} represents a Sensibo Sky unit timer definition
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class Timer {
public final int secondsRemaining;
public final AcState acState;
public final boolean enabled;
public Timer(TimerDTO dto) {
this.secondsRemaining = dto.targetTimeSecondsFromNow;
this.acState = new AcState(dto.acState);
this.enabled = dto.enabled;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="sensibo" 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>Sensibo Binding</name>
<description>This is the binding for Sensibo products</description>
<author>Arne Seime</author>
</binding:binding>

View File

@@ -0,0 +1,28 @@
<?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:sensibo:account">
<parameter name="apiKey" type="text" required="true">
<label>API Key</label>
<description>Your Sensibo app API key</description>
</parameter>
<parameter name="refreshInterval" type="integer" min="30" unit="s">
<label>Refresh Interval</label>
<description>How often to fetch updates from Sensibo service (polling interval)</description>
<default>120</default>
</parameter>
</config-description>
<config-description uri="thing-type:sensibo:sensibosky">
<parameter name="macAddress" type="text" required="true">
<label>MAC Address</label>
<description>With or without colons</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

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

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sensibo"
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="currentHumidity">
<item-type>Number:Dimensionless</item-type>
<label>Current Humidity</label>
<category>Humidity</category>
<tags>
<tag>CurrentHumidity</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="masterSwitch">
<item-type>Switch</item-type>
<label>Master Switch</label>
<tags>
<tag>Switchable</tag>
</tags>
</channel-type>
<channel-type id="timer">
<item-type>Number</item-type>
<label>Off Timer</label>
<description>Number of seconds until turning off</description>
<state readOnly="false"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sensibo"
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="sensibosky">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>HVAC controller</label>
<channels>
<channel id="currentTemperature" typeId="currentTemperature"/>
<channel id="currentHumidity" typeId="currentHumidity"/>
<channel id="masterSwitch" typeId="masterSwitch"/>
<channel id="timer" typeId="timer"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:sensibo:sensibosky"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,75 @@
/**
* 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.sensibo.internal;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.lang.reflect.Type;
import java.time.ZonedDateTime;
import org.apache.commons.io.IOUtils;
import org.openhab.binding.sensibo.internal.dto.AbstractRequest;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* @author Arne Seime - Initial contribution
*/
public class WireHelper {
private final Gson gson;
public WireHelper() {
gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, new TypeAdapter<ZonedDateTime>() {
@Override
public void write(final JsonWriter out, final ZonedDateTime value) throws IOException {
out.value(value.toString());
}
@Override
public ZonedDateTime read(final JsonReader in) throws IOException {
return ZonedDateTime.parse(in.nextString());
}
}).setPrettyPrinting().create();
}
public <T> T deSerializeResponse(final String jsonClasspathName, final Type type) throws IOException {
final String json = IOUtils.toString(WireHelper.class.getResourceAsStream(jsonClasspathName));
final JsonParser parser = new JsonParser();
final JsonObject o = parser.parse(json).getAsJsonObject();
assertEquals("success", o.get("status").getAsString());
return gson.fromJson(o.get("result"), type);
}
public <T> T deSerializeFromClasspathResource(final String jsonClasspathName, final Type type) throws IOException {
final String json = IOUtils.toString(WireHelper.class.getResourceAsStream(jsonClasspathName));
return deSerializeFromString(json, type);
}
public <T> T deSerializeFromString(final String json, final Type type) throws IOException {
return gson.fromJson(json, type);
}
public <T> String serialize(final AbstractRequest req) throws IOException {
return gson.toJson(req);
}
}

View File

@@ -0,0 +1,23 @@
/**
* 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.sensibo.internal.dto;
import org.openhab.binding.sensibo.internal.WireHelper;
/**
* @author Arne Seime - Initial contribution
*/
public abstract class AbstractSerializationDeserializationTest {
protected WireHelper wireHelper = new WireHelper();
}

View File

@@ -0,0 +1,103 @@
/**
* 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.sensibo.internal.dto;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.Map;
import org.junit.Test;
import org.openhab.binding.sensibo.internal.dto.poddetails.AcStateDTO;
import org.openhab.binding.sensibo.internal.dto.poddetails.MeasurementDTO;
import org.openhab.binding.sensibo.internal.dto.poddetails.ModeCapabilityDTO;
import org.openhab.binding.sensibo.internal.dto.poddetails.PodDetailsDTO;
import org.openhab.binding.sensibo.internal.dto.poddetails.TemperatureDTO;
import org.openhab.binding.sensibo.internal.model.SensiboSky;
/**
* @author Arne Seime - Initial contribution
*/
public class GetPodDetailsResponseTest extends AbstractSerializationDeserializationTest {
@Test
public void testDeserializeWithSmartModeSetup() throws IOException {
final PodDetailsDTO rsp = wireHelper.deSerializeResponse("/get_pod_details_response_smartmode_settings.json",
PodDetailsDTO.class);
assertEquals("34:15:13:AA:AA:AA", rsp.macAddress);
}
@Test
public void testDeserializeNullpointerExample() throws IOException {
final PodDetailsDTO rsp = wireHelper.deSerializeResponse("/get_pod_details_response_nullpointer.json",
PodDetailsDTO.class);
SensiboSky internal = new SensiboSky(rsp);
assertEquals("50175457", internal.getSerialNumber());
}
@Test
public void testDeserialize() throws IOException {
final PodDetailsDTO rsp = wireHelper.deSerializeResponse("/get_pod_details_response.json", PodDetailsDTO.class);
assertEquals("MA:C:AD:DR:ES:S0", rsp.macAddress);
assertEquals("IN010056", rsp.firmwareVersion);
assertEquals("cc3100_stm32f0", rsp.firmwareType);
assertEquals("SERIALNUMASSTRING", rsp.serialNumber);
assertEquals("C", rsp.temperatureUnit);
assertEquals("skyv2", rsp.productModel);
assertAcState(rsp.acState);
assertMeasurement(rsp.lastMeasurement);
assertRemoteCapabilities(rsp.getRemoteCapabilities());
}
private void assertRemoteCapabilities(final Map<String, ModeCapabilityDTO> remoteCapabilities) {
assertNotNull(remoteCapabilities);
assertEquals(5, remoteCapabilities.size());
final ModeCapabilityDTO mode = remoteCapabilities.get("heat");
assertNotNull(mode.swingModes);
assertNotNull(mode.fanLevels);
assertNotNull(mode.temperatures);
final Map<String, TemperatureDTO> temperatures = mode.temperatures;
final TemperatureDTO temperature = temperatures.get("C");
assertNotNull(temperature);
assertNotNull(temperature.validValues);
}
private void assertMeasurement(final MeasurementDTO lastMeasurement) {
assertNotNull(lastMeasurement);
assertNull(lastMeasurement.batteryVoltage);
assertEquals(Double.valueOf("22.5"), lastMeasurement.temperature);
assertEquals(Double.valueOf("24.2"), lastMeasurement.humidity);
assertEquals(Integer.valueOf("-71"), lastMeasurement.wifiSignalStrength);
assertEquals(ZonedDateTime.parse("2019-05-05T07:52:11Z"), lastMeasurement.measurementTimestamp.time);
}
private void assertAcState(final AcStateDTO acState) {
assertNotNull(acState);
assertTrue(acState.on);
assertEquals("medium_high", acState.fanLevel);
assertEquals("C", acState.temperatureUnit);
assertEquals(21, acState.targetTemperature.intValue());
assertEquals("heat", acState.mode);
assertEquals("rangeFull", acState.swing);
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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.sensibo.internal.dto;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.openhab.binding.sensibo.internal.dto.pods.PodDTO;
import com.google.gson.reflect.TypeToken;
/**
* @author Arne Seime - Initial contribution
*/
public class GetPodsResponseTest extends AbstractSerializationDeserializationTest {
@Test
public void testDeserialize() throws IOException {
final Type type = new TypeToken<ArrayList<PodDTO>>() {
}.getType();
final List<PodDTO> rsp = wireHelper.deSerializeResponse("/get_pods_response.json", type);
assertEquals(1, rsp.size());
assertEquals("PODID", rsp.get(0).id);
}
}

View File

@@ -0,0 +1,37 @@
/**
* 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.sensibo.internal.dto;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import org.junit.Test;
import org.openhab.binding.sensibo.internal.dto.setacstateproperty.SetAcStatePropertyRequest;
/**
* @author Arne Seime - Initial contribution
*/
public class SetAcStatePropertyRequestTest extends AbstractSerializationDeserializationTest {
@Test
public void testSerializeDeserialize() throws IOException {
SetAcStatePropertyRequest req = new SetAcStatePropertyRequest("PODID", "targetTemperature", "mode");
String serializedJson = wireHelper.serialize(req);
final SetAcStatePropertyRequest deSerializedRequest = wireHelper.deSerializeFromString(serializedJson,
SetAcStatePropertyRequest.class);
assertEquals("mode", deSerializedRequest.newValue);
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.sensibo.internal.dto;
import static org.junit.Assert.*;
import java.io.IOException;
import org.junit.Test;
import org.openhab.binding.sensibo.internal.dto.setacstateproperty.SetAcStatePropertyReponse;
/**
* @author Arne Seime - Initial contribution
*/
public class SetAcStatePropertyResponseTest extends AbstractSerializationDeserializationTest {
@Test
public void testDeserialize() throws IOException {
final SetAcStatePropertyReponse rsp = wireHelper.deSerializeResponse("/set_acstate_response.json",
SetAcStatePropertyReponse.class);
assertNotNull(rsp.acState);
assertTrue(rsp.acState.on);
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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.sensibo.internal.dto;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import java.io.IOException;
import org.junit.Test;
import org.openhab.binding.sensibo.internal.dto.poddetails.AcStateDTO;
import org.openhab.binding.sensibo.internal.dto.settimer.SetTimerRequest;
/**
* @author Arne Seime - Initial contribution
*/
public class SetTimerRequestTest extends AbstractSerializationDeserializationTest {
@Test
public void testSerializeDeserialize() throws IOException {
AcStateDTO acState = new AcStateDTO(false, "fanLevel", "C", 21, "mode", "swing");
SetTimerRequest req = new SetTimerRequest("PODID", 60, acState);
String serializedJson = wireHelper.serialize(req);
final SetTimerRequest deSerializedRequest = wireHelper.deSerializeFromString(serializedJson,
SetTimerRequest.class);
assertNotNull(deSerializedRequest.acState);
assertEquals(60, deSerializedRequest.minutesFromNow);
assertFalse(deSerializedRequest.acState.on);
}
}

View File

@@ -0,0 +1,119 @@
/**
* 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.sensibo.internal.handler;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.eclipse.jetty.client.HttpClient;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.openhab.binding.sensibo.internal.config.SensiboAccountConfiguration;
import org.openhab.binding.sensibo.internal.model.SensiboSky;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
/**
* @author Arne Seime - Initial contribution
*/
public class SensiboAccountHandlerTest {
@Rule
public WireMockRule wireMockRule = new WireMockRule(WireMockConfiguration.options().dynamicPort());
@Mock
private Bridge sensiboAccountMock;
private HttpClient httpClient;
@Mock
private Configuration configuration;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
httpClient = new HttpClient();
httpClient.start();
SensiboAccountHandler.API_ENDPOINT = "http://localhost:" + wireMockRule.port() + "/api"; // https://home.sensibo.com/api/v2
}
@After
public void shutdown() throws Exception {
httpClient.stop();
}
@Test
public void testInitialize1() throws InterruptedException, IOException {
testInitialize("/get_pods_response.json", "/get_pod_details_response.json");
}
@Test
public void testInitializeMarco() throws InterruptedException, IOException {
testInitialize("/get_pods_response.json", "/get_pod_details_response_marco.json");
}
private void testInitialize(String podsResponse, String podDetailsResponse)
throws InterruptedException, IOException {
// Setup account
final SensiboAccountConfiguration accountConfig = new SensiboAccountConfiguration();
accountConfig.apiKey = "APIKEY";
when(configuration.as(eq(SensiboAccountConfiguration.class))).thenReturn(accountConfig);
// Setup initial response
final String getPodsResponse = IOUtils.toString(getClass().getResourceAsStream(podsResponse));
stubFor(get(urlEqualTo("/api/v2/users/me/pods?apiKey=APIKEY"))
.willReturn(aResponse().withStatus(200).withBody(getPodsResponse)));
// Setup 2nd response with details
final String getPodDetailsResponse = IOUtils.toString(getClass().getResourceAsStream(podDetailsResponse));
stubFor(get(urlEqualTo("/api/v2/pods/PODID?apiKey=APIKEY&fields=*"))
.willReturn(aResponse().withStatus(200).withBody(getPodDetailsResponse)));
when(sensiboAccountMock.getConfiguration()).thenReturn(configuration);
when(sensiboAccountMock.getUID()).thenReturn(new ThingUID("sensibo:account:thinguid"));
final SensiboAccountHandler subject = new SensiboAccountHandler(sensiboAccountMock, httpClient);
// Async, poll for status
subject.initialize();
// Verify num things found == 1
int numPods = 0;
for (int i = 0; i < 20; i++) {
final List<SensiboSky> things = subject.getModel().getPods();
numPods = things.size();
if (numPods == 1) {
break;
} else {
// Wait some more
Thread.sleep(200);
}
}
assertEquals(1, numPods);
}
}

View File

@@ -0,0 +1,138 @@
/**
* 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.sensibo.internal.handler;
import static org.junit.Assert.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.openhab.binding.sensibo.internal.SensiboBindingConstants;
import org.openhab.binding.sensibo.internal.SensiboCommunicationException;
import org.openhab.binding.sensibo.internal.WireHelper;
import org.openhab.binding.sensibo.internal.dto.poddetails.PodDetailsDTO;
import org.openhab.binding.sensibo.internal.handler.SensiboSkyHandler.StateChange;
import org.openhab.binding.sensibo.internal.model.SensiboModel;
import org.openhab.binding.sensibo.internal.model.SensiboSky;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
/**
* @author Arne Seime - Initial contribution
*/
public class SensiboSkyHandlerTest {
private final WireHelper wireHelper = new WireHelper();
@Test
public void testStateChangeValidation() throws IOException, SensiboCommunicationException {
final PodDetailsDTO rsp = wireHelper.deSerializeResponse("/get_pod_details_response.json", PodDetailsDTO.class);
SensiboSky sky = new SensiboSky(rsp);
Thing thing = Mockito.mock(Thing.class);
SensiboSkyHandler handler = new SensiboSkyHandler(thing);
// Target temperature
StateChange stateChangeCheck = handler.checkStateChangeValid(sky, SensiboSkyHandler.TARGET_TEMPERATURE_PROPERTY,
new DecimalType(123));
assertFalse(stateChangeCheck.valid);
assertNotNull(stateChangeCheck.validationMessage);
assertTrue(handler.checkStateChangeValid(sky, SensiboSkyHandler.TARGET_TEMPERATURE_PROPERTY,
new DecimalType(10)).valid);
// Mode
StateChange stateChangeCheckMode = handler.checkStateChangeValid(sky, "mode", "invalid");
assertFalse(stateChangeCheckMode.valid);
assertNotNull(stateChangeCheckMode.validationMessage);
assertTrue(handler.checkStateChangeValid(sky, "mode", "auto").valid);
// Swing
StateChange stateChangeCheckSwing = handler.checkStateChangeValid(sky, "swing", "invalid");
assertFalse(stateChangeCheckSwing.valid);
assertNotNull(stateChangeCheckSwing.validationMessage);
assertTrue(handler.checkStateChangeValid(sky, "swing", "stopped").valid);
// FanLevel
StateChange stateChangeCheckFanLevel = handler.checkStateChangeValid(sky, "fanLevel", "invalid");
assertFalse(stateChangeCheckFanLevel.valid);
assertNotNull(stateChangeCheckFanLevel.validationMessage);
assertTrue(handler.checkStateChangeValid(sky, "fanLevel", "high").valid);
}
@Test
public void testTemperatureConversion() throws IOException {
final PodDetailsDTO rsp = wireHelper.deSerializeResponse("/get_pod_details_response.json", PodDetailsDTO.class);
SensiboSky sky = new SensiboSky(rsp);
Thing thing = Mockito.mock(Thing.class);
Mockito.when(thing.getUID()).thenReturn(new ThingUID("sensibo:account:thinguid"));
Map<String, Object> config = new HashMap<>();
config.put("macAddress", sky.getMacAddress());
Mockito.when(thing.getConfiguration()).thenReturn(new Configuration(config));
SensiboSkyHandler handler = Mockito.spy(new SensiboSkyHandler(thing));
handler.initialize();
SensiboModel model = new SensiboModel(0);
model.addPod(sky);
// Once with Celcius argument
handler.handleCommand(new ChannelUID(thing.getUID(), SensiboBindingConstants.CHANNEL_TARGET_TEMPERATURE),
new QuantityType<>(50, ImperialUnits.FAHRENHEIT), model);
// Once with Fahrenheit
handler.handleCommand(new ChannelUID(thing.getUID(), SensiboBindingConstants.CHANNEL_TARGET_TEMPERATURE),
new QuantityType<>(10, SIUnits.CELSIUS), model);
// Once with Decimal directly
handler.handleCommand(new ChannelUID(thing.getUID(), SensiboBindingConstants.CHANNEL_TARGET_TEMPERATURE),
new DecimalType(10), model);
ArgumentCaptor<DecimalType> valueCapture = ArgumentCaptor.forClass(DecimalType.class);
Mockito.verify(handler, Mockito.times(3)).updateAcState(ArgumentMatchers.eq(sky), ArgumentMatchers.anyString(),
valueCapture.capture());
assertEquals(new DecimalType(10), valueCapture.getValue());
}
@Test
public void testAddDynamicChannelsMarco() throws IOException, SensiboCommunicationException {
testAddDynamicChannels("/get_pod_details_response_marco.json");
}
@Test
public void testAddDynamicChannels() throws IOException, SensiboCommunicationException {
testAddDynamicChannels("/get_pod_details_response.json");
}
private void testAddDynamicChannels(String podDetailsResponse) throws IOException, SensiboCommunicationException {
final PodDetailsDTO rsp = wireHelper.deSerializeResponse(podDetailsResponse, PodDetailsDTO.class);
SensiboSky sky = new SensiboSky(rsp);
Thing thing = Mockito.mock(Thing.class);
Mockito.when(thing.getUID()).thenReturn(new ThingUID("sensibo:account:thinguid"));
SensiboSkyHandler handler = Mockito.spy(new SensiboSkyHandler(thing));
List<Channel> dynamicChannels = handler.createDynamicChannels(sky);
assertTrue(!dynamicChannels.isEmpty());
}
}

View File

@@ -0,0 +1,352 @@
{
"status": "success",
"result": {
"configGroup": "stable1",
"macAddress": "MA:C:AD:DR:ES:S0",
"cleanFiltersNotificationEnabled": true,
"room": {
"name": "Basement",
"icon": "den"
},
"firmwareType": "cc3100_stm32f0",
"productModel": "skyv2",
"sensorsCalibration": {
"temperature": 0,
"humidity": 0
},
"temperatureUnit": "C",
"isGeofenceOnExitEnabled": false,
"connectionStatus": {
"isAlive": true,
"lastSeen": {
"secondsAgo": 80,
"time": "2019-05-05T07:52:11Z"
}
},
"id": "PODID",
"acState": {
"on": true,
"fanLevel": "medium_high",
"temperatureUnit": "C",
"targetTemperature": 21,
"mode": "heat",
"swing": "rangeFull"
},
"smartMode": null,
"shouldShowFilterCleaningNotification": true,
"location": {
"latLon": [
59.0000000,
10.0000000
],
"updateTime": null,
"country": "Norway",
"createTime": {
"secondsAgo": 17857639,
"time": "2018-10-10T15:26:12Z"
},
"address": [
"Streetname",
"zip city",
"Norway"
],
"id": "ADDRESSID"
},
"currentlyAvailableFirmwareVersion": "IN010056",
"isClimateReactGeofenceOnExitEnabled": false,
"remoteCapabilities": {
"modes": {
"dry": {
"swing": [
"stopped",
"fixedTop",
"fixedMiddleTop",
"fixedMiddle",
"fixedMiddleBottom",
"fixedBottom",
"rangeFull"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31
]
},
"F": {
"isNative": false,
"values": [
61,
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86,
88
]
}
},
"fanLevels": [
"quiet",
"low",
"medium",
"medium_high",
"high",
"auto"
]
},
"auto": {
"swing": [
"stopped",
"fixedTop",
"fixedMiddleTop",
"fixedMiddle",
"fixedMiddleBottom",
"fixedBottom",
"rangeFull"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31
]
},
"F": {
"isNative": false,
"values": [
61,
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86,
88
]
}
},
"fanLevels": [
"quiet",
"low",
"medium",
"medium_high",
"high",
"auto"
]
},
"heat": {
"swing": [
"stopped",
"fixedTop",
"fixedMiddleTop",
"fixedMiddle",
"fixedMiddleBottom",
"fixedBottom",
"rangeFull"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
10,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31
]
},
"F": {
"isNative": false,
"values": [
50,
61,
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86,
88
]
}
},
"fanLevels": [
"quiet",
"low",
"medium",
"medium_high",
"high",
"auto"
]
},
"fan": {
"swing": [
"stopped",
"fixedTop",
"fixedMiddleTop",
"fixedMiddle",
"fixedMiddleBottom",
"fixedBottom",
"rangeFull"
],
"temperatures": {},
"fanLevels": [
"quiet",
"low",
"medium",
"medium_high",
"high",
"auto"
]
},
"cool": {
"swing": [
"stopped",
"fixedTop",
"fixedMiddleTop",
"fixedMiddle",
"fixedMiddleBottom",
"fixedBottom",
"rangeFull"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31
]
},
"F": {
"isNative": false,
"values": [
61,
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86,
88
]
}
},
"fanLevels": [
"quiet",
"low",
"medium",
"medium_high",
"high",
"auto"
]
}
}
},
"serial": "SERIALNUMASSTRING",
"firmwareVersion": "IN010056",
"measurements": {
"batteryVoltage": null,
"temperature": 22.5,
"humidity": 24.2,
"time": {
"secondsAgo": 80,
"time": "2019-05-05T07:52:11Z"
},
"rssi": "-71",
"piezo": [
null,
null
]
}
}
}

View File

@@ -0,0 +1,416 @@
{
"status": "success",
"result": {
"configGroup": "stable",
"macAddress": "MA:C:AD:DR:ES:S0",
"isGeofenceOnExitEnabled": false,
"sensorsCalibration": {
"temperature": 0.0,
"humidity": 0.0
},
"cleanFiltersNotificationEnabled": true,
"connectionStatus": {
"isAlive": true,
"lastSeen": {
"secondsAgo": 51,
"time": "2019-10-08T04:40:03Z"
}
},
"acState": {
"on": false,
"mode": "dry",
"swing": "stopped"
},
"serial": "serial",
"id": "PODID",
"firmwareVersion": "IN010056",
"firmwareType": "cc3100_stm32f0",
"measurements": {
"batteryVoltage": null,
"temperature": 23.2,
"humidity": 59.5,
"time": {
"secondsAgo": 51,
"time": "2019-10-08T04:40:03Z"
},
"rssi": "-63",
"piezo": [
null,
null
]
},
"remoteFlavor": "Enormous Stegosaurus",
"smartMode": null,
"shouldShowFilterCleaningNotification": false,
"location": {
"latLon": [
41.9279707,
12.4625373
],
"updateTime": {
"secondsAgo": 75125215,
"time": "2017-05-21T16:33:59Z"
},
"name": "Casa di Marco",
"country": "Italia",
"createTime": {
"secondsAgo": 129385606,
"time": "2015-09-01T16:14:08Z"
},
"address": [
"XXX",
"XX",
"XXX"
],
"id": "XX"
},
"currentlyAvailableFirmwareVersion": "IN010056",
"tags": [],
"productModel": "skyv2",
"schedules": [
{
"nextTime": null,
"podUid": "PODUID",
"recurringDays": [],
"createTimeSecondsAgo": 67070291,
"isEnabled": false,
"createTime": "2017-08-22T22:02:43",
"acState": {
"on": true,
"fanLevel": "quiet",
"temperatureUnit": "C",
"targetTemperature": 26,
"mode": "cool"
},
"targetTimeLocal": "03:00",
"timezone": "Europe/Rome",
"nextTimeSecondsFromNow": null,
"causedBy": {
"username": "XX",
"firstName": "XX",
"lastName": "XX",
"email": "XXX@domain.it"
},
"id": "XXXXX"
},
{
"nextTime": null,
"podUid": "PODUID",
"recurringDays": [],
"createTimeSecondsAgo": 4605564,
"isEnabled": false,
"createTime": "2019-08-15T21:21:30",
"acState": {
"on": false
},
"targetTimeLocal": "05:30",
"timezone": "Europe/Rome",
"nextTimeSecondsFromNow": null,
"causedBy": {
"username": "XX",
"firstName": "XX",
"lastName": "XX",
"email": "XXX@domain.it"
},
"id": "QwNtU5zq2D"
},
{
"nextTime": null,
"podUid": "E69jFpsP",
"recurringDays": [],
"createTimeSecondsAgo": 67070257,
"isEnabled": false,
"createTime": "2017-08-22T22:03:17",
"acState": {
"on": false
},
"targetTimeLocal": "03:30",
"timezone": "Europe/Rome",
"nextTimeSecondsFromNow": null,
"causedBy": {
"username": "XX",
"firstName": "XX",
"lastName": "XX",
"email": "XXX@domain.it"
},
"id": "RwVeW8U3Hw"
},
{
"nextTime": null,
"podUid": "E69jFpsP",
"recurringDays": [],
"createTimeSecondsAgo": 4605590,
"isEnabled": false,
"createTime": "2019-08-15T21:21:04",
"acState": {
"on": true,
"fanLevel": "quiet",
"temperatureUnit": "C",
"targetTemperature": 26,
"mode": "cool"
},
"targetTimeLocal": "05:00",
"timezone": "Europe/Rome",
"nextTimeSecondsFromNow": null,
"causedBy": {
"username": "XX",
"firstName": "XX",
"lastName": "XX",
"email": "XXX@domain.it"
},
"id": "ruzhJCVBeW"
}
],
"isClimateReactGeofenceOnExitEnabled": false,
"remoteCapabilities": {
"modes": {
"dry": {
"temperatures": {
},
"swing": [
"stopped",
"rangeFull"
]
},
"auto": {
"swing": [
"stopped",
"rangeFull"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30
]
},
"F": {
"isNative": false,
"values": [
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86
]
}
},
"fanLevels": [
"quiet",
"low",
"medium_low",
"medium",
"medium_high",
"high",
"auto",
"strong"
]
},
"heat": {
"swing": [
"stopped",
"rangeFull"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30
]
},
"F": {
"isNative": false,
"values": [
50,
52,
54,
55,
57,
59,
61,
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86
]
}
},
"fanLevels": [
"quiet",
"low",
"medium_low",
"medium",
"medium_high",
"high",
"auto",
"strong"
]
},
"fan": {
"swing": [
"stopped",
"rangeFull"
],
"temperatures": {
},
"fanLevels": [
"quiet",
"low",
"medium_low",
"medium",
"medium_high",
"high",
"auto",
"strong"
]
},
"cool": {
"swing": [
"stopped",
"rangeFull"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30
]
},
"F": {
"isNative": false,
"values": [
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86
]
}
},
"fanLevels": [
"quiet",
"low",
"medium_low",
"medium",
"medium_high",
"high",
"auto",
"strong"
]
}
}
},
"remote": {
"window": false,
"toggle": false
},
"room": {
"name": "Camera da letto",
"icon": "Bedroom"
},
"temperatureUnit": "C",
"timer": {
"acState": {
"on": false,
"mode": "dry",
"swing": "stopped"
},
"targetTimeSecondsFromNow": -5275495,
"createTimeSecondsAgo": 5277295,
"isEnabled": false,
"causedBy": {
"username": "XX",
"firstName": "XX",
"lastName": "XX",
"email": "XXX@domain.it"
},
"id": "KWcppTmrbb",
"targetTime": "2019-08-08T03:15:59",
"createTime": "2019-08-08T02:45:59"
},
"motionSensors": [],
"remoteAlternatives": [
"_daikin2b_comfort",
"_daikin2f_33",
"_daikin2b_33",
"_daikin2b_33_comfort_horizontal_swinging",
"_daikin2b",
"_daikin2c",
"_daikin2bf_comfort",
"_daikin2_33",
"_daikin2f"
]
}
}

View File

@@ -0,0 +1,599 @@
{
"status": "success",
"result": {
"configGroup": "stable",
"macAddress": "xxxxxxxx",
"isGeofenceOnExitEnabled": false,
"sensorsCalibration": {
"temperature": 0.0,
"humidity": 0.0
},
"cleanFiltersNotificationEnabled": true,
"connectionStatus": {
"isAlive": true,
"lastSeen": {
"secondsAgo": 14,
"time": "2019-08-15T11:44:00Z"
}
},
"acState": {
"on": true,
"targetTemperature": 25,
"temperatureUnit": "C",
"mode": "cool",
"fanLevel": "auto"
},
"motionSensors": [],
"id": "aGbsMfYn",
"firmwareVersion": "IN010056",
"firmwareType": "cc3100_stm32f0",
"measurements": {
"temperature": 29.1,
"humidity": 71.5,
"time": {
"secondsAgo": 14,
"time": "2019-08-15T11:44:00Z"
},
"rssi": "-52",
"piezo": [
null,
null
]
},
"smartMode": {
"deviceUid": "aGbsMfYn",
"highTemperatureThreshold": 30.0,
"type": "temperature",
"lowTemperatureState": {
"on": false,
"fanLevel": "low",
"temperatureUnit": "C",
"targetTemperature": 24,
"mode": "cool"
},
"enabled": false,
"highTemperatureState": {
"on": true,
"fanLevel": "low",
"temperatureUnit": "C",
"targetTemperature": 24,
"mode": "cool"
},
"lowTemperatureThreshold": 25.0
},
"shouldShowFilterCleaningNotification": false,
"location": {
"latLon": [
12.9331702,
100.9190772
],
"updateTime": {
"secondsAgo": 11618978,
"time": "2019-04-03T00:14:36Z"
},
"name": "Karel\u0027s place",
"country": "Thailand",
"createTime": {
"secondsAgo": 48921206,
"time": "2018-01-26T06:30:48Z"
},
"address": [
"xxx",
"xxx",
"xxx"
],
"id": "2Cr6zmCY5W"
},
"currentlyAvailableFirmwareVersion": "IN010056",
"tags": [],
"productModel": "skyv2",
"isClimateReactGeofenceOnExitEnabled": false,
"remoteCapabilities": {
"modes": {
"dry": {
"temperatures": {
"C": {
"isNative": true,
"values": [
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30
]
},
"F": {
"isNative": false,
"values": [
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86
]
}
},
"fanLevels": [
"low",
"medium",
"high",
"auto"
]
},
"heat": {
"temperatures": {
"C": {
"isNative": true,
"values": [
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30
]
},
"F": {
"isNative": false,
"values": [
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86
]
}
},
"fanLevels": [
"low",
"medium",
"high",
"auto"
]
},
"fan": {
"temperatures": {
"C": {
"isNative": true,
"values": [
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30
]
},
"F": {
"isNative": false,
"values": [
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86
]
}
},
"fanLevels": [
"low",
"medium",
"high",
"auto"
]
},
"cool": {
"temperatures": {
"C": {
"isNative": true,
"values": [
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30
]
},
"F": {
"isNative": false,
"values": [
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86
]
}
},
"fanLevels": [
"low",
"medium",
"high",
"auto"
]
}
}
},
"serial": "50175457",
"remote": {
"window": false,
"toggle": false
},
"room": {
"name": "AC_bedroom",
"icon": "bedroom"
},
"temperatureUnit": "C",
"remoteFlavor": "Unofficial Cobra",
"remoteAlternatives": []
}
}

View File

@@ -0,0 +1,428 @@
{
"status": "success",
"result": {
"configGroup": "stable",
"macAddress": "00:11:22:33:44:55",
"isGeofenceOnExitEnabled": false,
"sensorsCalibration": {
"temperature": 0.0,
"humidity": 0.0
},
"cleanFiltersNotificationEnabled": true,
"connectionStatus": {
"isAlive": true,
"lastSeen": {
"secondsAgo": 12,
"time": "2019-10-05T15:26:45.199202Z"
}
},
"acState": {
"on": true,
"fanLevel": "auto",
"temperatureUnit": "C",
"targetTemperature": 10,
"mode": "heat",
"swing": "rangeFull"
},
"serial": "000000000",
"id": "PODID",
"firmwareVersion": "SKY30043",
"firmwareType": "esp8266ex",
"measurements": {
"temperature": 19.1,
"humidity": 34.8,
"time": {
"secondsAgo": 12,
"time": "2019-10-05T15:26:45.199202Z"
},
"rssi": "-68",
"piezo": [
null,
null
]
},
"remoteFlavor": "Enthusiastic Triceratops",
"shouldShowFilterCleaningNotification": false,
"location": {
"latLon": [
59.924477,
10.5884129
],
"name": "Home",
"country": "Fantasyland",
"createTime": {
"secondsAgo": 31104046,
"time": "2018-10-10T15:26:12Z"
},
"address": [
"Street",
"Zip post",
"ENgland"
],
"id": "XXXXXXXX"
},
"currentlyAvailableFirmwareVersion": "SKY30043",
"tags": [],
"productModel": "skyv2",
"schedules": [
{
"nextTime": "2019-10-06T11:00:00",
"podUid": "XXXXXX",
"recurringDays": [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
],
"createTimeSecondsAgo": 51,
"isEnabled": true,
"createTime": "2019-10-05T15:26:07",
"acState": {
"on": true,
"fanLevel": "auto",
"temperatureUnit": "C",
"targetTemperature": 10,
"mode": "heat"
},
"targetTimeLocal": "13:00",
"timezone": "Europe/Oslo",
"nextTimeSecondsFromNow": 70381,
"causedBy": {
"username": "username",
"firstName": "FIrst",
"lastName": "Last",
"email": "mail@gmail.com"
},
"id": "scheduleid"
}
],
"isClimateReactGeofenceOnExitEnabled": false,
"remoteCapabilities": {
"modes": {
"dry": {
"swing": [
"stopped",
"fixedTop",
"fixedMiddleTop",
"fixedMiddle",
"fixedMiddleBottom",
"fixedBottom",
"rangeFull"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31
]
},
"F": {
"isNative": false,
"values": [
61,
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86,
88
]
}
},
"fanLevels": [
"quiet",
"low",
"medium",
"medium_high",
"high",
"auto"
]
},
"auto": {
"swing": [
"stopped",
"fixedTop",
"fixedMiddleTop",
"fixedMiddle",
"fixedMiddleBottom",
"fixedBottom",
"rangeFull"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31
]
},
"F": {
"isNative": false,
"values": [
61,
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86,
88
]
}
},
"fanLevels": [
"quiet",
"low",
"medium",
"medium_high",
"high",
"auto"
]
},
"heat": {
"swing": [
"stopped",
"fixedTop",
"fixedMiddleTop",
"fixedMiddle",
"fixedMiddleBottom",
"fixedBottom",
"rangeFull"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
10,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31
]
},
"F": {
"isNative": false,
"values": [
50,
61,
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86,
88
]
}
},
"fanLevels": [
"quiet",
"low",
"medium",
"medium_high",
"high",
"auto"
]
},
"fan": {
"swing": [
"stopped",
"fixedTop",
"fixedMiddleTop",
"fixedMiddle",
"fixedMiddleBottom",
"fixedBottom",
"rangeFull"
],
"temperatures": {
},
"fanLevels": [
"quiet",
"low",
"medium",
"medium_high",
"high",
"auto"
]
},
"cool": {
"swing": [
"stopped",
"fixedTop",
"fixedMiddleTop",
"fixedMiddle",
"fixedMiddleBottom",
"fixedBottom",
"rangeFull"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31
]
},
"F": {
"isNative": false,
"values": [
61,
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86,
88
]
}
},
"fanLevels": [
"quiet",
"low",
"medium",
"medium_high",
"high",
"auto"
]
}
}
},
"remote": {
"window": false,
"toggle": false
},
"room": {
"name": "Stue",
"icon": "Livingroom"
},
"temperatureUnit": "C",
"timer": {
"acState": {
"on": false,
"fanLevel": "auto",
"temperatureUnit": "C",
"targetTemperature": 10,
"mode": "heat",
"swing": "rangeFull"
},
"targetTimeSecondsFromNow": 3142,
"createTimeSecondsAgo": 38,
"isEnabled": true,
"causedBy": {
"username": "arneseime",
"firstName": "Arne",
"lastName": "Seime",
"email": "arne.seime@gmail.com"
},
"id": "7AZN7A9amL",
"targetTime": "2019-10-05T16:42:11",
"createTime": "2019-10-05T15:49:11"
},
"motionSensors": [],
"remoteAlternatives": [
"_mitsubishi1f_horizontal_rangeful",
"_mitsubishi1_for_ben_ho",
"_mitsubishi1_plasma_on",
"_mitsubishi1f_fixed_left",
"_mitsubishi1_plasma_on_clean_on",
"mitsubishi1f",
"_mitsubishi1f_fixed_right",
"_mitsubishi1_left_swing_fixed_bottom",
"_mitsubishi1_fixed_center",
"_mitsubishi1f"
]
}
}

View File

@@ -0,0 +1,307 @@
{
"status": "success",
"result": {
"configGroup": "stable",
"macAddress": "34:15:13:AA:AA:AA",
"cleanFiltersNotificationEnabled": true,
"room": {
"name": "Living Room",
"icon": "Den"
},
"firmwareType": "cc3100_stm32f0",
"productModel": "skyv2",
"sensorsCalibration": {
"temperature": 0.0,
"humidity": 0.0
},
"temperatureUnit": "C",
"isGeofenceOnExitEnabled": true,
"connectionStatus": {
"isAlive": true,
"lastSeen": {
"secondsAgo": 17,
"time": "2019-05-12T22:24:36Z"
}
},
"id": "PODID",
"acState": {
"on": false,
"fanLevel": "high",
"temperatureUnit": "C",
"targetTemperature": 21,
"mode": "cool",
"swing": "rangeFull"
},
"smartMode": {
"deviceUid": "PODID",
"highTemperatureThreshold": 28.0,
"type": "temperature",
"lowTemperatureState": {
"on": false,
"fanLevel": "auto",
"temperatureUnit": "C",
"targetTemperature": 26,
"mode": "cool"
},
"enabled": false,
"highTemperatureState": {
"on": true,
"fanLevel": "auto",
"temperatureUnit": "C",
"targetTemperature": 26,
"mode": "cool"
},
"lowTemperatureThreshold": 20.0
},
"shouldShowFilterCleaningNotification": false,
"location": {
"latLon": [
47.5274799,
19.1127283
],
"country": "Hungary",
"createTime": {
"secondsAgo": 2519470,
"time": "2019-04-13T18:33:43Z"
},
"address": [
"B",
"u",
"d"
],
"id": "w9s6KMRrhR"
},
"currentlyAvailableFirmwareVersion": "IN010056",
"isClimateReactGeofenceOnExitEnabled": false,
"remoteCapabilities": {
"modes": {
"dry": {
"swing": [
"stopped",
"rangeFull",
"fixedBottom",
"fixedMiddleBottom",
"fixedMiddle",
"fixedMiddleTop",
"fixedTop"
],
"temperatures": {},
"fanLevels": [
"low"
]
},
"auto": {
"swing": [
"stopped",
"rangeFull",
"fixedBottom",
"fixedMiddleBottom",
"fixedMiddle",
"fixedMiddleTop",
"fixedTop"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30
]
},
"F": {
"isNative": false,
"values": [
59,
61,
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86
]
}
},
"fanLevels": [
"auto"
]
},
"heat": {
"swing": [
"stopped",
"rangeFull",
"fixedBottom",
"fixedMiddleBottom",
"fixedMiddle",
"fixedMiddleTop",
"fixedTop"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30
]
},
"F": {
"isNative": false,
"values": [
61,
63,
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86
]
}
},
"fanLevels": [
"low",
"medium_low",
"medium",
"medium_high",
"high",
"auto"
]
},
"fan": {
"swing": [
"stopped",
"rangeFull",
"fixedBottom",
"fixedMiddleBottom",
"fixedMiddle",
"fixedMiddleTop",
"fixedTop"
],
"temperatures": {},
"fanLevels": [
"low",
"medium_low",
"medium",
"medium_high",
"high",
"auto"
]
},
"cool": {
"swing": [
"stopped",
"rangeFull",
"fixedBottom",
"fixedMiddleBottom",
"fixedMiddle",
"fixedMiddleTop",
"fixedTop"
],
"temperatures": {
"C": {
"isNative": true,
"values": [
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30
]
},
"F": {
"isNative": false,
"values": [
64,
66,
68,
70,
72,
73,
75,
77,
79,
81,
82,
84,
86
]
}
},
"fanLevels": [
"low",
"medium_low",
"medium",
"medium_high",
"high",
"auto"
]
}
}
},
"serial": "11184989",
"firmwareVersion": "IN010056",
"measurements": {
"temperature": 23.8,
"humidity": 58.9,
"time": {
"secondsAgo": 17,
"time": "2019-05-12T22:24:36Z"
},
"rssi": "-40",
"piezo": [
null,
null
]
}
}
}

View File

@@ -0,0 +1,8 @@
{
"status": "success",
"result": [
{
"id": "PODID"
}
]
}

View File

@@ -0,0 +1,10 @@
{ "acState": {
"on": true,
"fanLevel": "medium_high",
"temperatureUnit": "C",
"targetTemperature": 21,
"mode": "heat",
"swing": "rangeFull"
}
}

View File

@@ -0,0 +1,18 @@
{
"status": "success",
"result": {
"status": "Success",
"reason": "UserRequest",
"acState": {
"on": true,
"fanLevel": "medium_high",
"temperatureUnit": "C",
"targetTemperature": 21,
"mode": "heat",
"swing": "rangeFull"
},
"changedProperties": [],
"id": "RESULTID",
"failureReason": null
}
}