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,32 @@
<?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="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" 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.robonect</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,118 @@
# Robonect Binding
Robonect is a piece of hardware which has to be put into your Husqvarna, Gardena and other branded automower and makes
it accessible in your internal network.
More details about the Robonect module can be found at [robonect.de](https://forum.robonect.de/)
This binding integrates mowers having the robonect module installed as a thing into the home automation solution, allowing to
control the mower and react on mower status changes in rules.
## Supported Things
The binding exposes just one Thing type which is the `mower`.
Tested mowers
| Mower | Robonect module | Robonect firmware version |
|-------------------------|------------------|---------------------------|
| Husqvarna Automower 105 | Robonect Hx | 0.9c, 0.9e |
| Husqvarna Automower 315 | Robonect Hx | 0.9e, 1.0 preview |
| Husqvarna Automower 320 | Robonect Hx | 1.0 Beta7a |
| Husqvarna Automower 420 | Robonect Hx | 0.9e, 1.0 Beta2 |
| Gardena SILENO city 250 | Robonect Hx | 1.2 |
## Discovery
Robonect does not support automatic discovery. So the thing has to be added manually either via Paper UI or things configuration.
## Thing Configuration
following configuration settings are supported for the `mower` thing.
| parameter name | mandatory | description |
|----------------|-----------|---------------------------------------------------------------------------------------------------|
| host | yes | the hostname or ip address of the mower. |
| pollInterval | no | the interval for the binding to poll for mower status information. |
| offlineTimeout | no | the maximum time, the mower can be offline before the binding triggers the offlineTrigger channel |
| user | no | the username if authentication is enabled in the firmware. |
| password | no | the password if authenticaiton is enabled in the firmware. |
| timezone | no | the timezone as configured in Robonect on the robot (default: Europe/Berlin) |
An example things configuration might look like
```java
Thing robonect:mower:automower "Mower" @ "Garden" [ host="192.168.2.1", pollInterval="5", user="gardener", password = "cutter"]
```
## Channels
| Channel ID | Item Type | Description |
|------------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name` | String | Retrieves or sets the name of the mower |
| `battery` | Number | Retrieves the current battery status in percent |
| `status-duration` | Number | Retrieves the duration of the current status (see `status`) of the mower |
| `mowing-hours` | Number | Retrieves the number of hours of mowing operation |
| `mode` | String | Retrieves or sets the mode of the mower. Possible values retrieval values are <ul><li>HOME</li><li>AUTO</li><li>MANUAL</li><li>EOD : triggers the "end of day" mode. The mower will switch in to the HOME mode and stay int this mode for the rest of the day. After midnight it will switch back to the mode which was set previously.</li></ul> |
| `status` | Number | Retrieves the current mower status which can be <ul><li>0 : DETECTING_STATUS</li><li>1 : PARKING</li><li>2 : MOWING</li><li>3 : SEARCH_CHARGING_STATION</li><li>4 : CHARGING</li><li>5 : SEARCHING</li><li>6 : UNKNOWN_6</li><li>7 : ERROR_STATUS</li><li>16 : OFF</li><li>17 : SLEEPING</li><li>98 : OFFLINE (Binding cannot connect to mower)</li><li>99 : UNKNOWN</li></ul> |
| `start` | Switch | Starts the mower. ON is started (analog to pressing the start button on mower) or OFF (analog to the stop button on mower). |
| `job` | Switch | Starts a job. The channels can be configured with the three parameters `remoteStart`, `afterMode` and `duration`. `remoteStart` defines the mowing start point with the corresponding options `REMOTE_1`, `REMOTE_2` and `DEFAULT`. `afterMode` is the mode the mower will be set after the job is done. Allowed values are `AUTO`, `HOME` or `EOD`. `duration` is the job duration in minutes. Please note, if the mower is charging it will wait to start the job until it is fully charged, but the jobs duration is already started.|
| `timer-status` | String | Retrieves the status of the timer which can be <ul><li>INACTIVE : no timer set</li><li>ACTIVE - timer set and currently running</li><li>STANDBY - timer set but not triggered/running yet</li></ul> |
| `timer-next` | DateTime | Retrieves the Date and Time of the next timer set. This is just valid if there is an ACTIVE timer status (see `timer-status`). |
| `wlan-signal` | Number | Retrieves the current WLAN Signal strength in dB |
| `error-code` | Number | The mower manufacturer code in case the mower is in status 7 (error). The binding resets this to UNDEF, once the mower is not in error status anymore. |
| `error-message` | String | The error message in case the mower is in status 7 (error). The binding resets this to UNDEF, once the mower is not in error status anymore. |
| `error-date ` | DateTime | The date and time the error happened. The binding resets this to UNDEF, once the mower is not in error status anymore. |
| `last-error-code` | Number | The mower manufacturer code of the last error happened |
| `last-error-message` | String | The error message of the last error happened |
| `last-error-date ` | DateTime | The date and time of the last error happened |
| `health-temperature` | Number | The temperature of the mower (just available for robonect firmware >= 1.0) |
| `health-humidity ` | Number | The humidity of the mower (just available for robonect firmware >= 1.0) |
### Offline Trigger Channel
This channel s triggered if the mower is longer than the configured `offlineTriggerTimeout` offline.
This may indicate that the mower may stuck somewhere in error state but does not have a signal.
## Full Example
Things file `.things`
```java
Thing robonect:mower:automower "Mower" @ "Garden" [ host="192.168.2.1", pollInterval=5, user="gardener", password = "cutter"]
```
Items file `.items`
```java
String mowerName "Mower name" {channel="robonect:mower:automower:name"}
Number mowerBattery "Mower battery [%d %%]" <energy> {channel="robonect:mower:automower:battery"}
Number mowerHours "Mower operation hours [%d h]" <clock> {channel="robonect:mower:automower:mowing-hours"}
Number mowerDuration "Duration of current mode" {channel="robonect:mower:automower:status-duration"}
String mowerMode "Mower mode" {channel="robonect:mower:automower:mode"}
Number mowerStatus "Mower Status [MAP(robonect_status.map):%s]" {channel="robonect:mower:automower:status"}
Switch mowerStarted "Mower started" {channel="robonect:mower:automower:started"}
String mowerTimerStatus "Mower timer status" {channel="robonect:mower:automower:timer-status"}
DateTime mowerNextTimer "Next timer [%1$td/%1$tm %1$tH:%1$tM]" <clock> {channel="robonect:mower:automower:timer-next"}
Number mowerWlanSignal "WLAN signal [%d dB ]" {channel="robonect:mower:automower:wlan-signal"}
Switch mowerOneHourJob "Start mowing for one hour from now" {channel="robonect:mower:automower:job",remoteStart=REMOTE_1,afterMode=AUTO,duration=60}
Number mowerErrorCode "Error code" {channel="robonect:mower:automower:error-code"}
String mowerErrorMessage "Error message" {channel="robonect:mower:automower:error-message"}
DateTime mowerErrorDate "Error date [%1$td/%1$tm %1$tH:%1$tM]" {channel="robonect:mower:automower:error-date"}
```
Map transformation for mower status (`robonect_status.map`)
```text
0=DETECTING_STATUS
1=PARKING
2=MOWING
3=SEARCH_CHARGING_STATION
4=CHARGING
5=SEARCHING
7=ERROR_STATUS
8=LOST_SIGNAL
16=OFF
17=SLEEPING
99=UNKNOWN
```

View File

@@ -0,0 +1,17 @@
<?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/xsd/maven-4.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.robonect</artifactId>
<name>openHAB Add-ons :: Bundles :: Robonect Binding</name>
</project>

View File

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

View File

@@ -0,0 +1,58 @@
/**
* 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.robonect.internal;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link RobonectBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Marco Meyer - Initial contribution
*/
public class RobonectBindingConstants {
private static final String BINDING_ID = "robonect";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_AUTOMOWER = new ThingTypeUID(BINDING_ID, "mower");
// List of all Channel ids
public static final String CHANNEL_MOWER_NAME = "name";
public static final String CHANNEL_STATUS_BATTERY = "battery";
public static final String CHANNEL_STATUS_DURATION = "status-duration";
public static final String CHANNEL_STATUS_HOURS = "mowing-hours";
public static final String CHANNEL_STATUS_MODE = "mode";
public static final String CHANNEL_STATUS = "status";
public static final String CHANNEL_MOWER_START = "start";
public static final String CHANNEL_MOWER_STATUS_OFFLINE_TRIGGER = "offlineTrigger";
public static final String CHANNEL_TIMER_STATUS = "timer-status";
public static final String CHANNEL_TIMER_NEXT_TIMER = "timer-next";
public static final String CHANNEL_WLAN_SIGNAL = "wlan-signal";
public static final String CHANNEL_JOB = "job";
public static final String CHANNEL_ERROR_CODE = "error-code";
public static final String CHANNEL_ERROR_MESSAGE = "error-message";
public static final String CHANNEL_ERROR_DATE = "error-date";
public static final String CHANNEL_LAST_ERROR_CODE = "last-error-code";
public static final String CHANNEL_LAST_ERROR_MESSAGE = "last-error-message";
public static final String CHANNEL_LAST_ERROR_DATE = "last-error-date";
public static final String CHANNEL_HEALTH_TEMP = "health-temperature";
public static final String CHANNEL_HEALTH_HUM = "health-humidity";
public static final String PROPERTY_COMPILED = "compiled";
public static final String PROPERTY_COMMENT = "comment";
}

View File

@@ -0,0 +1,352 @@
/**
* 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.robonect.internal;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.B64Code;
import org.openhab.binding.robonect.internal.model.ErrorList;
import org.openhab.binding.robonect.internal.model.ModelParser;
import org.openhab.binding.robonect.internal.model.MowerInfo;
import org.openhab.binding.robonect.internal.model.MowerMode;
import org.openhab.binding.robonect.internal.model.Name;
import org.openhab.binding.robonect.internal.model.RobonectAnswer;
import org.openhab.binding.robonect.internal.model.VersionInfo;
import org.openhab.binding.robonect.internal.model.cmd.Command;
import org.openhab.binding.robonect.internal.model.cmd.ErrorCommand;
import org.openhab.binding.robonect.internal.model.cmd.ModeCommand;
import org.openhab.binding.robonect.internal.model.cmd.NameCommand;
import org.openhab.binding.robonect.internal.model.cmd.StartCommand;
import org.openhab.binding.robonect.internal.model.cmd.StatusCommand;
import org.openhab.binding.robonect.internal.model.cmd.StopCommand;
import org.openhab.binding.robonect.internal.model.cmd.VersionCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link RobonectClient} class is responsible to communicate with the robonect module via it's HTTP interface.
*
* The API of the module is documented here: http://robonect.de/viewtopic.php?f=10&t=37
*
* @author Marco Meyer - Initial contribution
*/
public class RobonectClient {
private final Logger logger = LoggerFactory.getLogger(RobonectClient.class);
private final String baseUrl;
private final HttpClient httpClient;
private final ModelParser parser;
private boolean jobRunning;
/**
* The {@link JobSettings} class holds the values required for starting a job.
*/
public static class JobSettings {
private static final String TIME_REGEX = "^[012]\\d:\\d\\d$";
private final Logger logger = LoggerFactory.getLogger(RobonectClient.class);
private ModeCommand.RemoteStart remoteStart;
private ModeCommand.Mode after;
private int duration;
/**
* returns the 'remote start' setting for the job. See {@link ModeCommand.RemoteStart} for details.
*
* @return - the remote start settings for the job.
*/
public ModeCommand.RemoteStart getRemoteStart() {
if (remoteStart != null) {
return remoteStart;
} else {
logger.debug("No explicit remote start set. Return STANDARD.");
return ModeCommand.RemoteStart.STANDARD;
}
}
/**
* Sets the desired 'remote start' settings for the job.
*
* @param remoteStart - The 'remote start' settings. See {@link ModeCommand.RemoteStart} for the allowed modes.
*/
public JobSettings withRemoteStart(ModeCommand.RemoteStart remoteStart) {
this.remoteStart = remoteStart;
return this;
}
/**
* Returns the mode the mower should be set to after the job is complete.
*
* @return - the mode after compleness of the job.
*/
public ModeCommand.Mode getAfterMode() {
return after;
}
/**
* Sets the mode after the mower is complete with the job.
*
* @param after - the desired mode after job completeness.
*/
public JobSettings withAfterMode(ModeCommand.Mode after) {
this.after = after;
return this;
}
public int getDuration() {
return duration;
}
public JobSettings withDuration(int duration) {
this.duration = duration;
return this;
}
}
private static class BasicResult implements Authentication.Result {
private final HttpHeader header;
private final URI uri;
private final String value;
public BasicResult(HttpHeader header, URI uri, String value) {
this.header = header;
this.uri = uri;
this.value = value;
}
public URI getURI() {
return this.uri;
}
public void apply(Request request) {
request.header(this.header, this.value);
}
public String toString() {
return String.format("Basic authentication result for %s", this.uri);
}
}
/**
* Creates an instance of RobonectClient which allows to communicate with the specified endpoint via the passed
* httpClient instance.
*
* @param httpClient - The HttpClient to use for the communication.
* @param endpoint - The endpoint information for connecting and issuing commands.
*/
public RobonectClient(HttpClient httpClient, RobonectEndpoint endpoint) {
this.httpClient = httpClient;
this.baseUrl = "http://" + endpoint.getIpAddress() + "/json";
this.parser = new ModelParser();
if (endpoint.isUseAuthentication()) {
addPreemptiveAuthentication(httpClient, endpoint);
}
}
private void addPreemptiveAuthentication(HttpClient httpClient, RobonectEndpoint endpoint) {
AuthenticationStore auth = httpClient.getAuthenticationStore();
URI uri = URI.create(baseUrl);
auth.addAuthenticationResult(new BasicResult(HttpHeader.AUTHORIZATION, uri, "Basic "
+ B64Code.encode(endpoint.getUser() + ":" + endpoint.getPassword(), StandardCharsets.ISO_8859_1)));
}
/**
* returns general mower information. See {@MowerInfo} for the detailed information.
*
* @return - the general mower information including a general success status.
*/
public MowerInfo getMowerInfo() {
String responseString = sendCommand(new StatusCommand());
MowerInfo mowerInfo = parser.parse(responseString, MowerInfo.class);
if (jobRunning) {
// mode might have been changed on the mower. Also Mode JOB does not really exist on the mower, thus cannot
// be checked here
if (mowerInfo.getStatus().getMode() == MowerMode.AUTO
|| mowerInfo.getStatus().getMode() == MowerMode.HOME) {
jobRunning = false;
} else if (mowerInfo.getError() != null) {
jobRunning = false;
}
}
return mowerInfo;
}
/**
* sends a start command to the mower.
*
* @return - a general answer with success status.
*/
public RobonectAnswer start() {
String responseString = sendCommand(new StartCommand());
return parser.parse(responseString, RobonectAnswer.class);
}
/**
* sends a stop command to the mower.
*
* @return - a general answer with success status.
*/
public RobonectAnswer stop() {
String responseString = sendCommand(new StopCommand());
return parser.parse(responseString, RobonectAnswer.class);
}
/**
* resets the errors on the mower.
*
* @return - a general answer with success status.
*/
public RobonectAnswer resetErrors() {
String responseString = sendCommand(new ErrorCommand().withReset(true));
return parser.parse(responseString, RobonectAnswer.class);
}
/**
* returns the list of all errors happened since last reset.
*
* @return - the list of errors.
*/
public ErrorList errorList() {
String responseString = sendCommand(new ErrorCommand());
return parser.parse(responseString, ErrorList.class);
}
/**
* Sets the mode of the mower. See {@link ModeCommand.Mode} for details about the available modes. Not allowed is
* mode
* {@link ModeCommand.Mode#JOB}.
*
* @param mode - the desired mower mode.
* @return - a general answer with success status.
*/
public RobonectAnswer setMode(ModeCommand.Mode mode) {
String responseString = sendCommand(createCommand(mode));
if (jobRunning) {
jobRunning = false;
}
return parser.parse(responseString, RobonectAnswer.class);
}
private ModeCommand createCommand(ModeCommand.Mode mode) {
return new ModeCommand(mode);
}
/**
* Returns the name of the mower.
*
* @return - The name including a general answer with success status.
*/
public Name getName() {
String responseString = sendCommand(new NameCommand());
return parser.parse(responseString, Name.class);
}
/**
* Allows to set the name of the mower.
*
* @param name - the desired name.
* @return - The resulting name including a general answer with success status.
*/
public Name setName(String name) {
String responseString = sendCommand(new NameCommand().withNewName(name));
return parser.parse(responseString, Name.class);
}
private String sendCommand(Command command) {
try {
if (logger.isDebugEnabled()) {
logger.debug("send HTTP GET to: {} ", command.toCommandURL(baseUrl));
}
ContentResponse response = httpClient.newRequest(command.toCommandURL(baseUrl)).method(HttpMethod.GET)
.timeout(30000, TimeUnit.MILLISECONDS).send();
String responseString = null;
// jetty uses UTF-8 as default encoding. However, HTTP 1.1 specifies ISO_8859_1
if (StringUtils.isBlank(response.getEncoding())) {
responseString = new String(response.getContent(), StandardCharsets.ISO_8859_1);
} else {
// currently v0.9e Robonect does not specifiy the encoding. But if later versions will
// add, it should work with the default method to get the content as string.
responseString = response.getContentAsString();
}
if (logger.isDebugEnabled()) {
logger.debug("Response body was: {} ", responseString);
}
return responseString;
} catch (ExecutionException | TimeoutException | InterruptedException e) {
throw new RobonectCommunicationException("Could not send command " + command.toCommandURL(baseUrl), e);
}
}
/**
* Retrieve the version information of the mower and module. See {@link VersionInfo} for details.
*
* @return - the Version Information including the successful status.
*/
public VersionInfo getVersionInfo() {
String versionResponse = sendCommand(new VersionCommand());
return parser.parse(versionResponse, VersionInfo.class);
}
public boolean isJobRunning() {
return jobRunning;
}
public RobonectAnswer startJob(JobSettings settings) {
Command jobCommand = new ModeCommand(ModeCommand.Mode.JOB).withRemoteStart(settings.remoteStart)
.withAfter(settings.after).withDuration(settings.duration);
String responseString = sendCommand(jobCommand);
RobonectAnswer answer = parser.parse(responseString, RobonectAnswer.class);
if (answer.isSuccessful()) {
jobRunning = true;
} else {
jobRunning = false;
}
return answer;
}
public RobonectAnswer stopJob(JobSettings settings) {
RobonectAnswer answer = null;
if (jobRunning) {
answer = setMode(settings.after);
if (answer.isSuccessful()) {
jobRunning = false;
}
} else {
answer = new RobonectAnswer();
// this is not an error, thus return success
answer.setSuccessful(true);
}
return answer;
}
}

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.robonect.internal;
/**
* This exception is thrown if there was an error in communication with the mower. As a mower is a moving object, this
* error is kind of expected and the error situation is handled in the handler.
*
* @author Marco Meyer - Initial contribution
*/
public class RobonectCommunicationException extends RuntimeException {
public RobonectCommunicationException(String message) {
super(message);
}
public RobonectCommunicationException(String message, Throwable cause) {
super(message, cause);
}
}

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.robonect.internal;
import org.eclipse.jetty.util.StringUtil;
/**
* The {@link RobonectEndpoint} is holds the information required to a Robonect endpoint.
*
* @author Marco Meyer - Initial contribution
*/
public class RobonectEndpoint {
private final String ipAddress;
private final String user;
private final String password;
private boolean useAuthentication = false;
public RobonectEndpoint(String ipAddress, String user, String password) {
this.ipAddress = ipAddress;
this.user = user;
this.password = password;
this.useAuthentication = StringUtil.isNotBlank(user) && StringUtil.isNotBlank(password);
}
public String getIpAddress() {
return ipAddress;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
public boolean isUseAuthentication() {
return useAuthentication;
}
}

View File

@@ -0,0 +1,69 @@
/**
* 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.robonect.internal;
import static org.openhab.binding.robonect.internal.RobonectBindingConstants.THING_TYPE_AUTOMOWER;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.robonect.internal.handler.RobonectHandler;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link RobonectHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Marco Meyer - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.robonect")
public class RobonectHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_AUTOMOWER);
private HttpClient httpClient;
private TimeZoneProvider timeZoneProvider;
@Activate
public RobonectHandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference TimeZoneProvider timeZoneProvider) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.timeZoneProvider = timeZoneProvider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_AUTOMOWER)) {
return new RobonectHandler(thing, httpClient, timeZoneProvider);
}
return null;
}
}

View File

@@ -0,0 +1,51 @@
/**
* 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.robonect.internal.config;
/**
* The {@link JobChannelConfig} class holds the channel configuration for the Job channel.
*
* @author Marco Meyer - Initial contribution
*/
public class JobChannelConfig {
private String remoteStart;
private String afterMode;
private int duration;
public String getRemoteStart() {
return remoteStart;
}
public void setRemoteStart(String remoteStart) {
this.remoteStart = remoteStart;
}
public String getAfterMode() {
return afterMode;
}
public void setAfterMode(String afterMode) {
this.afterMode = afterMode;
}
public int getDuration() {
return duration;
}
public void setDuration(int duration) {
this.duration = duration;
}
}

View File

@@ -0,0 +1,58 @@
/**
* 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.robonect.internal.config;
/**
*
* This class acts simply a structure for holding the thing configuration.
*
* @author Marco Meyer - Initial contribution
*/
public class RobonectConfig {
private String host;
private String user;
private String password;
private int pollInterval;
private int offlineTimeout;
private String timezone;
public String getHost() {
return host;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
public int getPollInterval() {
return pollInterval;
}
public int getOfflineTimeout() {
return offlineTimeout;
}
public String getTimezone() {
return timezone;
}
}

View File

@@ -0,0 +1,446 @@
/**
* 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.robonect.internal.handler;
import static org.openhab.binding.robonect.internal.RobonectBindingConstants.*;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.robonect.internal.RobonectClient;
import org.openhab.binding.robonect.internal.RobonectCommunicationException;
import org.openhab.binding.robonect.internal.RobonectEndpoint;
import org.openhab.binding.robonect.internal.config.JobChannelConfig;
import org.openhab.binding.robonect.internal.config.RobonectConfig;
import org.openhab.binding.robonect.internal.model.ErrorEntry;
import org.openhab.binding.robonect.internal.model.ErrorList;
import org.openhab.binding.robonect.internal.model.MowerInfo;
import org.openhab.binding.robonect.internal.model.Name;
import org.openhab.binding.robonect.internal.model.RobonectAnswer;
import org.openhab.binding.robonect.internal.model.VersionInfo;
import org.openhab.binding.robonect.internal.model.cmd.ModeCommand;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/**
* The {@link RobonectHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* The channels are periodically updated by polling the mower via HTTP in a separate thread.
*
* @author Marco Meyer - Initial contribution
*/
public class RobonectHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(RobonectHandler.class);
private ScheduledFuture<?> pollingJob;
private HttpClient httpClient;
private TimeZoneProvider timeZoneProvider;
private ZoneId timeZone;
private RobonectClient robonectClient;
public RobonectHandler(Thing thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
super(thing);
this.httpClient = httpClient;
this.timeZoneProvider = timeZoneProvider;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
try {
if (command instanceof RefreshType) {
refreshChannels(channelUID);
} else {
sendCommand(channelUID, command);
}
updateStatus(ThingStatus.ONLINE);
} catch (RobonectCommunicationException rce) {
logger.debug("Failed to communicate with the mower. Taking it offline.", rce);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, rce.getMessage());
}
}
private void sendCommand(ChannelUID channelUID, Command command) {
switch (channelUID.getId()) {
case CHANNEL_MOWER_NAME:
if (command instanceof StringType) {
updateName((StringType) command);
} else {
logger.debug("Got name update of type {} but StringType is expected.",
command.getClass().getName());
}
break;
case CHANNEL_STATUS_MODE:
if (command instanceof StringType) {
setMowerMode(command);
} else {
logger.debug("Got job remote start update of type {} but StringType is expected.",
command.getClass().getName());
}
break;
case CHANNEL_MOWER_START:
if (command instanceof OnOffType) {
handleStartStop((OnOffType) command);
} else {
logger.debug("Got stopped update of type {} but OnOffType is expected.",
command.getClass().getName());
}
break;
case CHANNEL_JOB:
if (command instanceof OnOffType) {
handleJobCommand(channelUID, command);
} else {
logger.debug("Got job update of type {} but OnOffType is expected.", command.getClass().getName());
}
break;
}
}
private void handleJobCommand(ChannelUID channelUID, Command command) {
JobChannelConfig jobConfig = getThing().getChannel(channelUID.getId()).getConfiguration()
.as(JobChannelConfig.class);
if (command == OnOffType.ON) {
robonectClient.startJob(
new RobonectClient.JobSettings().withAfterMode(ModeCommand.Mode.valueOf(jobConfig.getAfterMode()))
.withRemoteStart(ModeCommand.RemoteStart.valueOf(jobConfig.getRemoteStart()))
.withDuration(jobConfig.getDuration()));
} else if (command == OnOffType.OFF) {
robonectClient.stopJob(
new RobonectClient.JobSettings().withAfterMode(ModeCommand.Mode.valueOf(jobConfig.getAfterMode())));
}
}
private void refreshChannels(ChannelUID channelUID) {
switch (channelUID.getId()) {
case CHANNEL_MOWER_NAME:
case CHANNEL_STATUS_BATTERY:
case CHANNEL_STATUS:
case CHANNEL_STATUS_DURATION:
case CHANNEL_STATUS_HOURS:
case CHANNEL_STATUS_MODE:
case CHANNEL_MOWER_START:
case CHANNEL_TIMER_NEXT_TIMER:
case CHANNEL_TIMER_STATUS:
case CHANNEL_WLAN_SIGNAL:
case CHANNEL_JOB:
refreshMowerInfo();
break;
default:
case CHANNEL_LAST_ERROR_CODE:
case CHANNEL_LAST_ERROR_DATE:
case CHANNEL_LAST_ERROR_MESSAGE:
refreshLastErrorInfo();
break;
}
}
private void setMowerMode(Command command) {
String modeStr = command.toFullString();
ModeCommand.Mode newMode = ModeCommand.Mode.valueOf(modeStr.toUpperCase());
if (robonectClient.setMode(newMode).isSuccessful()) {
updateState(CHANNEL_STATUS_MODE, new StringType(newMode.name()));
} else {
refreshMowerInfo();
}
}
private void logErrorFromResponse(RobonectAnswer result) {
if (!result.isSuccessful()) {
logger.debug("Could not send EOD Trigger. Robonect error message: {}", result.getErrorMessage());
}
}
private void handleStartStop(final OnOffType command) {
RobonectAnswer answer = null;
boolean currentlyStopped = robonectClient.getMowerInfo().getStatus().isStopped();
if (command == OnOffType.ON && currentlyStopped) {
answer = robonectClient.start();
} else if (command == OnOffType.OFF && !currentlyStopped) {
answer = robonectClient.stop();
}
if (answer != null) {
if (answer.isSuccessful()) {
updateState(CHANNEL_MOWER_START, command);
} else {
logErrorFromResponse(answer);
refreshMowerInfo();
}
}
}
private void updateName(StringType command) {
String newName = command.toFullString();
Name name = robonectClient.setName(newName);
if (name.isSuccessful()) {
updateState(CHANNEL_MOWER_NAME, new StringType(name.getName()));
} else {
logErrorFromResponse(name);
refreshMowerInfo();
}
}
private void refreshMowerInfo() {
MowerInfo info = robonectClient.getMowerInfo();
if (info.isSuccessful()) {
if (info.getError() != null) {
updateErrorInfo(info.getError());
refreshLastErrorInfo();
} else {
clearErrorInfo();
}
updateState(CHANNEL_MOWER_NAME, new StringType(info.getName()));
updateState(CHANNEL_STATUS_BATTERY, new DecimalType(info.getStatus().getBattery()));
updateState(CHANNEL_STATUS, new DecimalType(info.getStatus().getStatus().getStatusCode()));
updateState(CHANNEL_STATUS_DURATION,
new QuantityType<>(info.getStatus().getDuration(), SmartHomeUnits.SECOND));
updateState(CHANNEL_STATUS_HOURS, new QuantityType<>(info.getStatus().getHours(), SmartHomeUnits.HOUR));
updateState(CHANNEL_STATUS_MODE, new StringType(info.getStatus().getMode().name()));
updateState(CHANNEL_MOWER_START, info.getStatus().isStopped() ? OnOffType.OFF : OnOffType.ON);
if (info.getHealth() != null) {
updateState(CHANNEL_HEALTH_TEMP,
new QuantityType<>(info.getHealth().getTemperature(), SIUnits.CELSIUS));
updateState(CHANNEL_HEALTH_HUM,
new QuantityType(info.getHealth().getHumidity(), SmartHomeUnits.PERCENT));
}
if (info.getTimer() != null) {
if (info.getTimer().getNext() != null) {
updateNextTimer(info);
}
updateState(CHANNEL_TIMER_STATUS, new StringType(info.getTimer().getStatus().name()));
}
updateState(CHANNEL_WLAN_SIGNAL, new DecimalType(info.getWlan().getSignal()));
updateState(CHANNEL_JOB, robonectClient.isJobRunning() ? OnOffType.ON : OnOffType.OFF);
} else {
logger.error("Could not retrieve mower info. Robonect error response message: {}", info.getErrorMessage());
}
}
private void clearErrorInfo() {
updateState(CHANNEL_ERROR_DATE, UnDefType.UNDEF);
updateState(CHANNEL_ERROR_CODE, UnDefType.UNDEF);
updateState(CHANNEL_ERROR_MESSAGE, UnDefType.UNDEF);
}
private void updateErrorInfo(ErrorEntry error) {
if (error.getErrorMessage() != null) {
updateState(CHANNEL_ERROR_MESSAGE, new StringType(error.getErrorMessage()));
}
if (error.getErrorCode() != null) {
updateState(CHANNEL_ERROR_CODE, new DecimalType(error.getErrorCode().intValue()));
}
if (error.getDate() != null) {
State dateTime = convertUnixToDateTimeType(error.getUnix());
updateState(CHANNEL_ERROR_DATE, dateTime);
}
}
private void updateNextTimer(MowerInfo info) {
State dateTime = convertUnixToDateTimeType(info.getTimer().getNext().getUnix());
updateState(CHANNEL_TIMER_NEXT_TIMER, dateTime);
}
private State convertUnixToDateTimeType(String unixTimeSec) {
// the value in unixTimeSec represents the time on the robot in its configured timezone. However, it does not
// provide which zone this is. Thus we have to add the zone information from the Thing configuration in order to
// provide correct results.
Instant rawInstant = Instant.ofEpochMilli(Long.valueOf(unixTimeSec) * 1000);
ZoneId timeZoneOfThing = timeZone;
if (timeZoneOfThing == null) {
timeZoneOfThing = timeZoneProvider.getTimeZone();
}
ZoneOffset offsetToConfiguredZone = timeZoneOfThing.getRules().getOffset(rawInstant);
long adjustedTime = rawInstant.getEpochSecond() - offsetToConfiguredZone.getTotalSeconds();
Instant adjustedInstant = Instant.ofEpochMilli(adjustedTime * 1000);
// we provide the time in the format as configured in the openHAB settings
ZonedDateTime zdt = adjustedInstant.atZone(timeZoneProvider.getTimeZone());
return new DateTimeType(zdt);
}
private void refreshVersionInfo() {
VersionInfo info = robonectClient.getVersionInfo();
if (info.isSuccessful()) {
Map<String, String> properties = editProperties();
properties.put(Thing.PROPERTY_SERIAL_NUMBER, info.getRobonect().getSerial());
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, info.getRobonect().getVersion());
properties.put(PROPERTY_COMPILED, info.getRobonect().getCompiled());
properties.put(PROPERTY_COMMENT, info.getRobonect().getComment());
updateProperties(properties);
} else {
logger.debug("Could not retrieve mower version info. Robonect error response message: {}",
info.getErrorMessage());
}
}
private void refreshLastErrorInfo() {
ErrorList errorList = robonectClient.errorList();
if (errorList.isSuccessful()) {
List<ErrorEntry> errors = errorList.getErrors();
if (errors != null && !errors.isEmpty()) {
ErrorEntry lastErrorEntry = errors.get(0);
updateLastErrorChannels(lastErrorEntry);
}
} else {
logger.debug("Could not retrieve mower error list. Robonect error response message: {}",
errorList.getErrorMessage());
}
}
private void updateLastErrorChannels(ErrorEntry error) {
if (error.getErrorMessage() != null) {
updateState(CHANNEL_LAST_ERROR_MESSAGE, new StringType(error.getErrorMessage()));
}
if (error.getErrorCode() != null) {
updateState(CHANNEL_LAST_ERROR_CODE, new DecimalType(error.getErrorCode().intValue()));
}
if (error.getDate() != null) {
State dateTime = convertUnixToDateTimeType(error.getUnix());
updateState(CHANNEL_LAST_ERROR_DATE, dateTime);
}
}
@Override
public void initialize() {
RobonectConfig robonectConfig = getConfigAs(RobonectConfig.class);
RobonectEndpoint endpoint = new RobonectEndpoint(robonectConfig.getHost(), robonectConfig.getUser(),
robonectConfig.getPassword());
String timeZoneString = robonectConfig.getTimezone();
try {
if (timeZoneString != null) {
timeZone = ZoneId.of(timeZoneString);
} else {
logger.warn("No timezone provided, falling back to the default timezone configured in openHAB: '{}'",
timeZoneProvider.getTimeZone());
}
} catch (DateTimeException e) {
logger.warn("Error setting timezone '{}', falling back to the default timezone configured in openHAB: '{}'",
timeZoneString, timeZoneProvider.getTimeZone(), e);
}
try {
httpClient.start();
robonectClient = new RobonectClient(httpClient, endpoint);
} catch (Exception e) {
logger.error("Exception while trying to start http client", e);
throw new RuntimeException("Exception while trying to start http client", e);
}
Runnable runnable = new MowerChannelPoller(TimeUnit.SECONDS.toMillis(robonectConfig.getOfflineTimeout()));
int pollInterval = robonectConfig.getPollInterval();
pollingJob = scheduler.scheduleWithFixedDelay(runnable, 0, pollInterval, TimeUnit.SECONDS);
}
@Override
public void dispose() {
if (pollingJob != null) {
pollingJob.cancel(true);
pollingJob = null;
}
try {
if (httpClient != null) {
httpClient.stop();
httpClient = null;
}
} catch (Exception e) {
logger.debug("Could not stop http client", e);
}
}
/**
* method to inject the robonect client to be used in test cases to allow mocking.
*
* @param robonectClient
*/
protected void setRobonectClient(RobonectClient robonectClient) {
this.robonectClient = robonectClient;
}
private class MowerChannelPoller implements Runnable {
private long offlineSince;
private long offlineTriggerDelay;
private boolean offlineTimeoutTriggered;
private boolean loadVersionInfo = true;
public MowerChannelPoller(long offlineTriggerDelay) {
offlineSince = -1;
this.offlineTriggerDelay = offlineTriggerDelay;
offlineTimeoutTriggered = false;
}
@Override
public void run() {
try {
if (loadVersionInfo) {
refreshVersionInfo();
loadVersionInfo = false;
}
refreshMowerInfo();
updateStatus(ThingStatus.ONLINE);
offlineSince = -1;
offlineTimeoutTriggered = false;
} catch (RobonectCommunicationException rce) {
if (offlineSince < 0) {
offlineSince = System.currentTimeMillis();
}
if (!offlineTimeoutTriggered && System.currentTimeMillis() - offlineSince > offlineTriggerDelay) {
// trigger offline
updateState(CHANNEL_MOWER_STATUS_OFFLINE_TRIGGER, new StringType("OFFLINE_TIMEOUT"));
offlineTimeoutTriggered = true;
}
logger.debug("Failed to communicate with the mower. Taking it offline.", rce);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, rce.getMessage());
loadVersionInfo = true;
} catch (JsonSyntaxException jse) {
// the module sporadically sends invalid json responses. As this is usually recovered with the
// next poll interval, we just log it to debug here.
logger.debug("Failed to parse response.", jse);
}
}
}
}

View File

@@ -0,0 +1,90 @@
/**
* 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.robonect.internal.model;
import com.google.gson.annotations.SerializedName;
/**
* POJO for deserialize an error entry from a JSON response using GSON.
*
* @author Marco Meyer - Initial contribution
*/
public class ErrorEntry {
private String date;
@SerializedName("error_code")
private Integer errorCode;
@SerializedName("error_message")
private String errorMessage;
private String time;
private String unix;
/**
* @return - the date the error happend in the format "dd.MM.yy"
*/
public String getDate() {
return date;
}
/**
* @return - the error code. Some codes are documented here: http://www.robonect.de/viewtopic.php?f=11&t=110
*/
public Integer getErrorCode() {
return errorCode;
}
/**
* @return - The localized error message from the mower.
*/
public String getErrorMessage() {
return errorMessage;
}
/**
* @return - The time the error happened in the format "HH:mm:ss"
*/
public String getTime() {
return time;
}
/**
* @return - The unix time when the error happened.
*/
public String getUnix() {
return unix;
}
public void setDate(String date) {
this.date = date;
}
public void setErrorCode(Integer errorCode) {
this.errorCode = errorCode;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public void setTime(String time) {
this.time = time;
}
public void setUnix(String unix) {
this.unix = unix;
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.robonect.internal.model;
import java.util.List;
/**
* Simple POJO for deserialize the list of errors from the errors command.
*
* @author Marco Meyer - Initial contribution
*/
public class ErrorList extends RobonectAnswer {
private List<ErrorEntry> errors;
/**
* @return - the list of errors.
*/
public List<ErrorEntry> getErrors() {
return errors;
}
}

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.robonect.internal.model;
/**
* Health information from the mower. This information is just included if the robonect module runs the firmware
* 1.0 beta or higher.
*
* @author Marco Meyer - Initial contribution
*/
public class Health {
private int temperature;
private int humidity;
/**
* @return - the temperature in °C measured in the mower.
*/
public int getTemperature() {
return temperature;
}
/**
* @return - the humidity in % measured in the mower.
*/
public int getHumidity() {
return humidity;
}
}

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.robonect.internal.model;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* This class is responsible for parsing JSNON formatted answers from the Robonect module using the Gson library.
*
* @author Marco Meyer - Initial contribution
*/
public class ModelParser {
private final Gson gson;
/**
* Creates a parser with containing a preconfigured Gson object capable of parsing the JSON answers from the
* Robonect module.
*/
public ModelParser() {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(MowerStatus.class, new MowerStatusDeserializer());
gsonBuilder.registerTypeAdapter(MowerMode.class, new MowerModeDeserializer());
gsonBuilder.registerTypeAdapter(Timer.TimerMode.class, new TimerModeDeserializer());
this.gson = gsonBuilder.create();
}
/**
* Parses a jsonString to a Java Object of the specified type.
*
* @param jsonString - the json string to parse
* @param type - the class of the type of the expected object to be returned.
* @param <T> - the type of expected return value.
* @return
*/
public <T> T parse(String jsonString, Class<T> type) {
return gson.fromJson(jsonString, type);
}
}

View File

@@ -0,0 +1,95 @@
/**
* 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.robonect.internal.model;
/**
* The mower information holds the main information from the majority of the available channels. This class is a POJO
* to deserialize the JSON response from the module.
*
* @author Marco Meyer - Initial contribution
*/
public class MowerInfo extends RobonectAnswer {
private String name;
private Status status;
private Timer timer;
private Wlan wlan;
private Health health;
private ErrorEntry error;
/**
* @return - the name of the mower
*/
public String getName() {
return name;
}
/**
* @return - some status information of the mower. See {@link Status} for details.
*/
public Status getStatus() {
return status;
}
/**
* @return - the current timer status information.
*/
public Timer getTimer() {
return timer;
}
/**
* @return - the WLAN signal status.
*/
public Wlan getWlan() {
return wlan;
}
/**
* @return - if the mower is in error status {@link #getStatus()} the error information is returned, null otherwise.
*/
public ErrorEntry getError() {
return error;
}
/**
* @return - the health status information.
*/
public Health getHealth() {
return health;
}
public void setName(String name) {
this.name = name;
}
public void setStatus(Status status) {
this.status = status;
}
public void setTimer(Timer timer) {
this.timer = timer;
}
public void setWlan(Wlan wlan) {
this.wlan = wlan;
}
public void setHealth(Health health) {
this.health = health;
}
public void setError(ErrorEntry error) {
this.error = error;
}
}

View File

@@ -0,0 +1,77 @@
/**
* 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.robonect.internal.model;
/**
* The mower mode from the status information. Please note
* that EOD and JOB from {@link org.openhab.binding.robonect.internal.model.cmd.ModeCommand.Mode}
* are just artificial and are therfore not reported in the status information.
*
* @author Marco Meyer - Initial contribution
*/
public enum MowerMode {
/**
* The AUTO mode
*/
AUTO(0),
/**
* The MANUAL mode
*/
MANUAL(1),
/**
* The HOME mode
*/
HOME(2),
/**
* The DEMO mode
*/
DEMO(3),
/**
* An unknown mode. Actually this mode should never be set but is there if for some reason an unexpected value
* is returned from the module response.
*/
UNKNOWN(99);
private int code;
MowerMode(int code) {
this.code = code;
}
/**
* Translate the numeric mode from the JSON response into enum values.
*
* @param mode - the numeric value of the mode.
* @return - the enum value of the mode.
*/
public static MowerMode fromMode(int mode) {
for (MowerMode mowerMode : MowerMode.values()) {
if (mowerMode.code == mode) {
return mowerMode;
}
}
return UNKNOWN;
}
/**
* @return - The numeric code of the mode.
*/
public int getCode() {
return code;
}
}

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.robonect.internal.model;
import java.lang.reflect.Type;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
* This is a Gson desirializer capable of translating numeric modes into enum values.
*
* @author Marco Meyer - Initial contribution
*/
public class MowerModeDeserializer implements JsonDeserializer<MowerMode> {
@Override
public MowerMode deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
int mode = jsonElement.getAsInt();
return MowerMode.fromMode(mode);
}
}

View File

@@ -0,0 +1,116 @@
/**
* 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.robonect.internal.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An enumeration for the possible mower status.
*
* @author Marco Meyer - Initial contribution
*/
public enum MowerStatus {
/**
* Status is being detected.
*/
DETECTING_STATUS(0),
/**
* Mower is in charging station.
*/
PARKING(1),
/**
* Mower is mowing.
*/
MOWING(2),
/**
* Mower searches charging station
*/
SEARCH_CHARGING_STATION(3),
/**
* Mower is charging.
*/
CHARGING(4),
/**
* Mower is searching the remote start point.
*/
SEARCHING(5),
/**
* Mower is in error state.
*/
ERROR_STATUS(7),
/**
* Mower lost WLAN signal.
*/
LOST_SIGNAL(8),
/**
* Mower is OFF.
*/
OFF(16),
/**
* Mower is sleeping
*/
SLEEPING(17),
/**
* Mower waits for door to open
*/
DOORDELAY(18),
/**
* unknown status. If the module return any not listed code here it will result in this state in the binding.
*/
UNKNOWN(99);
private static final Logger LOGGER = LoggerFactory.getLogger(MowerStatus.class);
private int statusCode;
MowerStatus(int statusCode) {
this.statusCode = statusCode;
}
/**
* translates a numeric code into an enum value. If code is not known the value {@link #UNKNOWN} is returned.
*
* @param code - the code to translate
* @return - the correpsonding enum value.
*/
public static MowerStatus fromCode(int code) {
for (MowerStatus status : MowerStatus.values()) {
if (status.statusCode == code) {
return status;
}
}
LOGGER.debug("Got an unknown state with code {}", code);
return UNKNOWN;
}
/**
* returns the numeric code of the status.
*
* @return
*/
public int getStatusCode() {
return statusCode;
}
}

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.robonect.internal.model;
import java.lang.reflect.Type;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
* This is a Gson deserializer to deserialize numeric mower status codes into enum values.
*
* @author Marco Meyer - Initial contribution
*/
public class MowerStatusDeserializer implements JsonDeserializer<MowerStatus> {
@Override
public MowerStatus deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
int code = jsonElement.getAsInt();
return MowerStatus.fromCode(code);
}
}

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.robonect.internal.model;
/**
* Response holding the name of the mower used in the name command.
*
* @author Marco Meyer - Initial contribution
*/
public class Name extends RobonectAnswer {
private String name;
/**
* @return - The mower name.
*/
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,58 @@
/**
* 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.robonect.internal.model;
/**
* This object will be part of the status response and holds the next timer execution information.
*
* @author Marco Meyer - Initial contribution
*/
public class NextTimer {
private String date;
private String time;
private String unix;
/**
* @return - The date (dd.MM.yy) of the next timer execution.
*/
public String getDate() {
return date;
}
/**
* @return - the timestamp (HH:mm:ss) of the next timer execution
*/
public String getTime() {
return time;
}
/**
* @return - the next timer execution in the form of a unix timestamp.
*/
public String getUnix() {
return unix;
}
public void setDate(String date) {
this.date = date;
}
public void setTime(String time) {
this.time = time;
}
public void setUnix(String unix) {
this.unix = unix;
}
}

View File

@@ -0,0 +1,63 @@
/**
* 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.robonect.internal.model;
import com.google.gson.annotations.SerializedName;
/**
* The super class of all answers from the robonect module. All answersd derive from this class. An answer is either
* successful where all the information of the subclass will be filled, or it is not successful, and this class will
* hold the error information.
*
* @author Marco Meyer - Initial contribution
*/
public class RobonectAnswer {
private boolean successful;
@SerializedName("error_code")
private Integer errorCode;
@SerializedName("error_message")
private String errorMessage;
/**
* @return - true if the request was successful, false otherwise.
*/
public boolean isSuccessful() {
return successful;
}
/**
* allows to set the successful status for testing.
*
* @param successful
*/
public void setSuccessful(boolean successful) {
this.successful = successful;
}
/**
* @return - in case of a not successful request, the error code, null otherwise.
*/
public Integer getErrorCode() {
return errorCode;
}
/**
* @return - in case of a not successful request, the error message, null otherwise.
*/
public String getErrorMessage() {
return errorMessage;
}
}

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.robonect.internal.model;
/**
* Object holding information from the status section of the mowers status response.
*
* @author Marco Meyer - Initial contribution
*/
public class Status {
private int battery;
private int duration;
private int hours;
private MowerStatus status;
private MowerMode mode;
private boolean stopped;
/**
* @return - the battery level in percent. (0-100)
*/
public int getBattery() {
return battery;
}
/**
* @return - The duration in seconds the mower is already in the current {@link #status}.
*/
public int getDuration() {
return duration;
}
/**
* @return - The hours the mower was in use so far.
*/
public int getHours() {
return hours;
}
/**
* @return - The status the mower is currently in. see {@link MowerStatus} for details.
*/
public MowerStatus getStatus() {
return status;
}
/**
* @return - true if the mower is currentyl stopped, false otherwise.
*/
public boolean isStopped() {
return stopped;
}
/**
* @return - The mode the mower is currently in. See {@link MowerMode} for details.
*/
public MowerMode getMode() {
return mode;
}
public void setBattery(int battery) {
this.battery = battery;
}
public void setDuration(int duration) {
this.duration = duration;
}
public void setHours(int hours) {
this.hours = hours;
}
public void setStatus(MowerStatus status) {
this.status = status;
}
public void setMode(MowerMode mode) {
this.mode = mode;
}
public void setStopped(boolean stopped) {
this.stopped = stopped;
}
}

View File

@@ -0,0 +1,86 @@
/**
* 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.robonect.internal.model;
/**
* An object holding the timer information of the mower status.
*
* @author Marco Meyer - Initial contribution
*/
public class Timer {
/**
* an enum defining the possible timer status.
*/
public enum TimerMode {
/**
* timer is inactive. No timer is set or the mower is not in AUTO mode.
*/
INACTIVE(0),
/**
* timer is active. The period of the timer is active and the mower is executing it in AUTO mode.
*/
ACTIVE(1),
/**
* timer is standby. A timer is set, the mower is in AUTO mode but the timer period did not start yet.
*/
STANDBY(2);
private int code;
TimerMode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static TimerMode fromCode(int code) {
for (TimerMode status : TimerMode.values()) {
if (status.code == code) {
return status;
}
}
return INACTIVE;
}
}
private TimerMode status;
private NextTimer next;
/**
* @return - the timer mode. see {@link TimerMode}
*/
public TimerMode getStatus() {
return status;
}
/**
* @return - information about when the next timer execution will be. See {@link NextTimer}
*/
public NextTimer getNext() {
return next;
}
public void setStatus(TimerMode status) {
this.status = status;
}
public void setNext(NextTimer next) {
this.next = next;
}
}

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.robonect.internal.model;
import java.lang.reflect.Type;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
* Gson deserializer for deserializing timer mode values to the corresponding enum.
*
* @author Marco Meyer - Initial contribution
*/
public class TimerModeDeserializer implements JsonDeserializer<Timer.TimerMode> {
@Override
public Timer.TimerMode deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
int code = jsonElement.getAsInt();
return Timer.TimerMode.fromCode(code);
}
}

View File

@@ -0,0 +1,111 @@
/**
* 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.robonect.internal.model;
/**
* Answer object for holding version information.
*
* @author Marco Meyer - Initial contribution
*/
public class VersionInfo extends RobonectAnswer {
private static final RobonectVersion NA_VERSION = new RobonectVersion();
/**
* encapsulates the robonect version information.
*/
public static class RobonectVersion {
private static final String NA = "n/a";
private String serial;
private String version;
private String compiled;
private String comment;
public RobonectVersion() {
this(NA, NA, NA, NA);
}
public RobonectVersion(String serial, String version, String compiled, String comment) {
this.serial = serial;
this.version = version;
this.compiled = compiled;
this.comment = comment;
}
/**
* @return - The serial number of the robonect module.
*/
public String getSerial() {
return serial;
}
/**
* @return - The firmware version running on the robonect module.
*/
public String getVersion() {
return version;
}
/**
* @return - The date and time the firmware was compiled.
*/
public String getCompiled() {
return compiled;
}
/**
* @return - The comment added to this version.
*/
public String getComment() {
return comment;
}
public void setSerial(String serial) {
this.serial = serial;
}
public void setVersion(String version) {
this.version = version;
}
public void setCompiled(String compiled) {
this.compiled = compiled;
}
public void setComment(String comment) {
this.comment = comment;
}
}
private RobonectVersion robonect;
/**
* @return - the object encapsulating the version information. See {@link RobonectVersion}
*/
public RobonectVersion getRobonect() {
if (robonect != null) {
return robonect;
} else {
return NA_VERSION;
}
}
public void setRobonect(RobonectVersion robonect) {
this.robonect = robonect;
}
}

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.robonect.internal.model;
/**
* Object holding the wlan signal strength.
*
* @author Marco Meyer - Initial contribution
*/
public class Wlan {
private int signal;
/**
* @return - The signal strength in dB.
*/
public int getSignal() {
return signal;
}
public void setSignal(int signal) {
this.signal = signal;
}
}

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.robonect.internal.model.cmd;
import org.openhab.binding.robonect.internal.RobonectClient;
/**
*
* Interface implemented by all commands. The robonect module is called with urls like
* http://xxx.xxx.xxx/json?cmd=[command]. The command implementation is responsible to construct the full command url.
*
*
* @author Marco Meyer - Initial contribution
*/
public interface Command {
/**
* Implementations of this interface have to return baseUrl + command specific extensions, where the baseURL
* already is in the form http://xxx.xxx.xxx/json?
*
* @param baseURL - will be passed by the {@link RobonectClient} in the form
* http://xxx.xxx.xxx/json?
* @return - the full command string like for example for a name command http://xxx.xxx.xxx/json?cmd=name
*/
String toCommandURL(String baseURL);
}

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.robonect.internal.model.cmd;
import org.openhab.binding.robonect.internal.RobonectClient;
/**
* Implementation of the error command allowing to retrieve the list of errors or resetting the list.
*
* @author Marco Meyer - Initial contribution
*/
public class ErrorCommand implements Command {
private boolean reset = false;
/**
* has to be set to 'true' if the errors should be reset.
*
* @param reset - true if errors should be reset, false if the list of errors should be retrieved.
* @return
*/
public ErrorCommand withReset(boolean reset) {
this.reset = reset;
return this;
}
/**
* @param baseURL - will be passed by the {@link RobonectClient} in the form
* http://xxx.xxx.xxx/json?
* @return - the command for retrieving or resetting the error list.
*/
@Override
public String toCommandURL(String baseURL) {
if (reset) {
return baseURL + "?cmd=error&reset";
} else {
return baseURL + "?cmd=error";
}
}
}

View File

@@ -0,0 +1,195 @@
/**
* 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.robonect.internal.model.cmd;
import org.openhab.binding.robonect.internal.RobonectClient;
/**
*
* The mode commands sets the mower into the corresponding mode. In addition to the mowers standard modes
* (HOME, MAN, AUTO) the module supports following modes:
*
* EOD (End Of Day): The mower is set into HOME mode until midnight. After midnight the module sets the mower in AUTO
* mode
* JOB: The JOB mode triggers a JOB and supports following additional parameter:
* * remoteStart: where to start the job (STANDARD, REMOTE_1 or REMOTE_2)
* * after: The mode to be set after the JOB is done. Allowed are all except JOB.
* * start: the start time in the form HH:MM (H=Hour,M=Minute)
* * end: the end time in the form HH:MM (H=Hour,M=Minute)
* * duration: mowing time in minutes (in combination with start or end time)
*
* @author Marco Meyer - Initial contribution
*/
public class ModeCommand implements Command {
/**
* The available modes. See class documentation for the meanings.
*/
public enum Mode {
HOME(1, "home"),
EOD(2, "eod"),
MANUAL(3, "man"),
AUTO(4, "auto"),
JOB(5, "job");
int code;
String cmd;
Mode(int code, String cmd) {
this.code = code;
this.cmd = cmd;
}
}
/**
* The available remoteStart values.
*/
public enum RemoteStart {
/**
* Start immediatly at the docking station.
*/
STANDARD(0),
/**
* Start at the configured remote 1 location.
*/
REMOTE_1(1),
/**
* Start at the conifugred remote 2 location.
*/
REMOTE_2(2);
int code;
RemoteStart(int code) {
this.code = code;
}
}
private Mode mode;
private RemoteStart remoteStart;
private Mode after;
private String start;
private String end;
private Integer duration;
public ModeCommand(Mode mode) {
this.mode = mode;
}
/**
* sets the desired remoteStart option.
*
* @param remoteStart - the remoteStart option.
* @return - the command instance.
*/
public ModeCommand withRemoteStart(RemoteStart remoteStart) {
this.remoteStart = remoteStart;
return this;
}
/**
* set the mode after the job is done.
*
* @param afterMode - the desired mode after job execution.
* @return - the command instance.
*/
public ModeCommand withAfter(Mode afterMode) {
this.after = afterMode;
return this;
}
/**
* The desired start time in the format HH:MM (H=Hour, M=Minute)
*
* @param startTime - the start time.
* @return - the command instance.
*/
public ModeCommand withStart(String startTime) {
this.start = startTime;
return this;
}
/**
* The desired end time in the format HH:MM (H=Hour, M=Minute)
*
* @param endTime - the end time.
* @return - the command instance.
*/
public ModeCommand withEnd(String endTime) {
this.end = endTime;
return this;
}
/**
* Sets the duration in minutes.
*
* @param durationInMinutes - the duration in minutes.
* @return - the command instance.
*/
public ModeCommand withDuration(Integer durationInMinutes) {
this.duration = durationInMinutes;
return this;
}
/**
* {@inheritDoc}
*
* @param baseURL - will be passed by the {@link RobonectClient} in the form
* http://xxx.xxx.xxx/json?
* @return
*/
@Override
public String toCommandURL(String baseURL) {
StringBuilder sb = new StringBuilder(baseURL);
sb.append("?cmd=mode&mode=");
sb.append(mode.cmd);
switch (mode) {
case EOD:
case MANUAL:
case AUTO:
case HOME:
break;
case JOB:
if (remoteStart != null) {
sb.append("&remotestart=");
sb.append(remoteStart.code);
}
if (after != null) {
sb.append("&after=");
sb.append(after.code);
}
if (start != null) {
sb.append("&start=");
sb.append(start);
}
if (end != null) {
sb.append("&end=");
sb.append(end);
}
if (duration != null) {
sb.append("&duration=");
sb.append(duration);
}
break;
}
return sb.toString();
}
}

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.robonect.internal.model.cmd;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import org.openhab.binding.robonect.internal.RobonectClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The command allows to set or retrieve the mower name.
*
* @author Marco Meyer - Initial contribution
*/
public class NameCommand implements Command {
private final Logger logger = LoggerFactory.getLogger(NameCommand.class);
private String newName;
/**
* sets the mower name.
*
* @param newName - the mower name.
* @return - the command instance.
*/
public NameCommand withNewName(String newName) {
this.newName = newName != null ? newName : "";
return this;
}
/**
* @param baseURL - will be passed by the {@link RobonectClient} in the form
* http://xxx.xxx.xxx/json?
* @return
*/
@Override
public String toCommandURL(String baseURL) {
if (newName == null) {
return baseURL + "?cmd=name";
} else {
try {
return baseURL + "?cmd=name&name="
+ URLEncoder.encode(newName, StandardCharsets.ISO_8859_1.displayName());
} catch (UnsupportedEncodingException e) {
logger.error("Could not encode name {} ", newName, e);
return baseURL + "?cmd=name";
}
}
}
}

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.robonect.internal.model.cmd;
import org.openhab.binding.robonect.internal.RobonectClient;
/**
* The command starts the mower if it was stopped.
*
* @author Marco Meyer - Initial contribution
*/
public class StartCommand implements Command {
/**
* {@inheritDoc}
*
* @param baseURL - will be passed by the {@link RobonectClient} in the form
* http://xxx.xxx.xxx/json?
* @return
*/
@Override
public String toCommandURL(String baseURL) {
return baseURL + "?cmd=start";
}
}

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.robonect.internal.model.cmd;
import org.openhab.binding.robonect.internal.model.MowerStatus;
/**
* Queries the mowers status. The status holds a lot of status information.
* See {@link MowerStatus}
* or the documentation at: http://www.robonect.de/viewtopic.php?f=11&t=38
*
* @author Marco Meyer - Initial contribution
*/
public class StatusCommand implements Command {
@Override
public String toCommandURL(String baseURL) {
return baseURL + "?cmd=status";
}
}

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.robonect.internal.model.cmd;
import org.openhab.binding.robonect.internal.RobonectClient;
/**
* Stops the mower if it was started.
*
* @author Marco Meyer - Initial contribution
*/
public class StopCommand implements Command {
/**
* {@inheritDoc}
*
* @param baseURL - will be passed by the {@link RobonectClient} in the form
* http://xxx.xxx.xxx/json?
* @return
*/
@Override
public String toCommandURL(String baseURL) {
return baseURL + "?cmd=stop";
}
}

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.robonect.internal.model.cmd;
import org.openhab.binding.robonect.internal.model.VersionInfo;
/**
* Queries version information about the mower and the module. See {@link VersionInfo}
* for more information.
*
* @author Marco Meyer - Initial contribution
*/
public class VersionCommand implements Command {
@Override
public String toCommandURL(String baseURL) {
return baseURL + "?cmd=version";
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="robonect" 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>Robonect Binding</name>
<description>This is the binding for Robonect.</description>
<author>Marco Meyer</author>
</binding:binding>

View File

@@ -0,0 +1,25 @@
<?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="channel-type:job:config">
<parameter name="remoteStart" type="text" pattern="^STANDARD|REMOTE_1|REMOTE_2$">
<label>Remote Start</label>
<description>The location to start the mowing job from.</description>
</parameter>
<parameter name="duration" type="integer" min="0" max="60" unit="m">
<context>time</context>
<label>Job Duration</label>
<description>The duration of the job.</description>
<default>0</default>
</parameter>
<parameter name="afterMode" type="text" pattern="^AUTO|HOME|EOD$">
<label>After Job Mode</label>
<description>The Mode to put the mower into after the job is done.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,250 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="robonect"
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="mower">
<label>Mower</label>
<description>Mower robot connected via robonect module</description>
<channels>
<channel id="name" typeId="nameType"/>
<channel id="battery" typeId="system.battery-level"/>
<channel id="wlan-signal" typeId="system.signal-strength"/>
<channel id="mowing-hours" typeId="hoursType"/>
<channel id="mode" typeId="modeType"/>
<channel id="start" typeId="startType"/>
<channel id="status" typeId="mowerStatusType"/>
<channel id="status-duration" typeId="durationType"/>
<channel id="timer-status" typeId="timerStatusType"/>
<channel id="timer-next" typeId="nextTimerType"/>
<channel id="job" typeId="jobType"/>
<channel id="offlineTrigger" typeId="offlineTriggerType"/>
<channel id="error-code" typeId="errorCodeType"/>
<channel id="error-message" typeId="errorMessageType"/>
<channel id="error-date" typeId="errorDateType"/>
<channel id="last-error-code" typeId="errorCodeType">
<label>Last Error Code</label>
<description>The Error code of the last error occurred</description>
</channel>
<channel id="last-error-message" typeId="errorMessageType">
<label>Last Error Message</label>
<description>The error message of the last error occurred</description>
</channel>
<channel id="last-error-date" typeId="errorDateType">
<label>Last Error Date</label>
<description>The date and time of the last error occurred</description>
</channel>
<channel id="health-temperature" typeId="temperatureType"/>
<channel id="health-humidity" typeId="humidityType"/>
</channels>
<properties>
<property name="vendor">robonect.de</property>
<property name="firmwareVersion">N/A</property>
<property name="compiled">N/A</property>
<property name="serialNumber">N/A</property>
</properties>
<config-description>
<parameter name="host" type="text" required="true">
<context>network-address</context>
<label>Host</label>
<description>Host name or network address of the Robonect module</description>
</parameter>
<parameter name="user" type="text" required="false">
<label>User</label>
<description>The user id if authentication has been enabled on the mower</description>
</parameter>
<parameter name="password" type="text" required="false">
<context>password</context>
<label>Password</label>
<description>The password if authentication has been enabled on the mower</description>
</parameter>
<parameter name="pollInterval" type="integer" required="false" unit="s">
<label>Polling Interval</label>
<default>30</default>
<description>The interval for the binding to poll the mowers status information.
</description>
</parameter>
<parameter name="offlineTimeout" type="integer" required="false" unit="m">
<label>Offline Timeout</label>
<default>30</default>
<description>The maximum time the mower may be offline before the offline trigger is triggered.
</description>
</parameter>
<parameter name="timezone" type="text" required="false">
<label>Timezone</label>
<default>Europe/Berlin</default>
<description>The timezone configured on the robot (e.g. Europe/Berlin).</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="serialType">
<item-type>String</item-type>
<label>Robonect Serial Number</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="versionType">
<item-type>String</item-type>
<label>Robonect Version</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="compiledType">
<item-type>String</item-type>
<label>Robonect Version</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="commentType">
<item-type>String</item-type>
<label>Robonect Firmware Comment</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="nameType">
<item-type>String</item-type>
<label>Mower Name</label>
<description>The name of the mower</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="durationType">
<item-type>Number:Time</item-type>
<label>Status Duration</label>
<description>The number of seconds the mower is in the current status.</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="hoursType">
<item-type>Number:Time</item-type>
<label>Total Mowing Hours</label>
<description>The number of total mowing hours</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="temperatureType">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>The temperature inside the mower</description>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="humidityType">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>The relative humidity in the mower in percent</description>
<state readOnly="true" pattern="%d %%"/>
</channel-type>
<channel-type id="modeType">
<item-type>String</item-type>
<label>Mower Mode</label>
<description>The mower mode</description>
<state readOnly="false">
<options>
<option value="AUTO">Auto</option>
<option value="HOME">Home</option>
<option value="MANUAL">Manual</option>
<option value="EOD">End of Day</option>
</options>
</state>
</channel-type>
<channel-type id="jobType">
<item-type>Switch</item-type>
<label>Mowing Job</label>
<description>Starts a mowing job</description>
<config-description-ref uri="channel-type:job:config"/>
</channel-type>
<channel-type id="timerStatusType">
<item-type>String</item-type>
<label>Timer Status</label>
<description>The status of the timer</description>
<state readOnly="true">
<options>
<option value="INACTIVE">Inactive</option>
<option value="ACTIVE">Active</option>
<option value="STANDBY">Standby</option>
</options>
</state>
</channel-type>
<channel-type id="mowerStatusType">
<item-type>Number</item-type>
<label>Mower Status</label>
<description>The status of the mower</description>
<state readOnly="true">
<options>
<option value="0">Detecting Status</option>
<option value="1">Parking</option>
<option value="2">Mowing</option>
<option value="3">Search Charging Station</option>
<option value="4">Charging</option>
<option value="5">Searching</option>
<option value="6">Unknown Status 6</option>
<option value="7">Error Status</option>
<option value="16">Off</option>
<option value="17">Sleeping</option>
<option value="99">Unknown</option>
</options>
</state>
</channel-type>
<channel-type id="startType">
<item-type>Switch</item-type>
<label>Start</label>
<description>On if started, Off if stopped</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="offlineTriggerType">
<kind>trigger</kind>
<label>Offline Trigger</label>
<event>
<options>
<option value="OFFLINE_TIMEOUT">Offline Timeout</option>
</options>
</event>
</channel-type>
<channel-type id="nextTimerType">
<item-type>DateTime</item-type>
<label>Next Timer Date</label>
<description>The next date the timer starts</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="errorCodeType">
<item-type>Number</item-type>
<label>Error Code</label>
<description>Error code defined by the mower manufacturer</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="errorMessageType">
<item-type>String</item-type>
<label>Error Message</label>
<description>The error message</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="errorDateType">
<item-type>DateTime</item-type>
<label>Error Date</label>
<description>The date and time the error occurred</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,211 @@
/**
* 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.robonect.internal;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.openhab.binding.robonect.internal.model.ErrorList;
import org.openhab.binding.robonect.internal.model.MowerInfo;
import org.openhab.binding.robonect.internal.model.Name;
import org.openhab.binding.robonect.internal.model.VersionInfo;
/**
* The goal of this class is to test the functionality of the RobonectClient,
* by mocking the module responses.
*
* @author Marco Meyer - Initial contribution
*/
public class RobonectClientTest {
private RobonectClient subject;
@Mock
private HttpClient httpClientMock;
@Mock
private ContentResponse responseMock;
@Mock
private Request requestMock;
@Before
public void init() {
MockitoAnnotations.initMocks(this);
RobonectEndpoint dummyEndPoint = new RobonectEndpoint("123.456.789.123", null, null);
subject = new RobonectClient(httpClientMock, dummyEndPoint);
}
@Test
public void shouldCallStatusCommand() throws InterruptedException, ExecutionException, TimeoutException {
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=status")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(responseMock);
when(responseMock.getEncoding()).thenReturn("utf8");
when(responseMock.getContentAsString()).thenReturn(
"{\"successful\": true, \"name\": \"Mein Automower\", \"status\": {\"status\": 17, \"stopped\": false, \"duration\": 4359, \"mode\": 0, \"battery\": 100, \"hours\": 29}, \"timer\": {\"status\": 2, \"next\": {\"date\": \"01.05.2017\", \"time\": \"19:00:00\", \"unix\": 1493665200}}, \"wlan\": {\"signal\": -76}}");
subject.getMowerInfo();
verify(httpClientMock, times(1)).newRequest("http://123.456.789.123/json?cmd=status");
}
@Test
public void shouldCallStartCommand() throws InterruptedException, ExecutionException, TimeoutException {
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=start")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(responseMock);
when(responseMock.getEncoding()).thenReturn("utf8");
when(responseMock.getContentAsString()).thenReturn("{\"successful\": true}");
subject.start();
verify(httpClientMock, times(1)).newRequest("http://123.456.789.123/json?cmd=start");
}
@Test
public void shouldCallStopCommand() throws InterruptedException, ExecutionException, TimeoutException {
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=stop")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(responseMock);
when(responseMock.getEncoding()).thenReturn("utf8");
when(responseMock.getContentAsString()).thenReturn("{\"successful\": true}");
subject.stop();
verify(httpClientMock, times(1)).newRequest("http://123.456.789.123/json?cmd=stop");
}
@Test
public void shouldResetErrors() throws InterruptedException, ExecutionException, TimeoutException {
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=error&reset")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(responseMock);
when(responseMock.getEncoding()).thenReturn("utf8");
when(responseMock.getContentAsString()).thenReturn("{\"successful\": true}");
subject.resetErrors();
verify(httpClientMock, times(1)).newRequest("http://123.456.789.123/json?cmd=error&reset");
}
@Test
public void shouldRetrieveName() throws InterruptedException, ExecutionException, TimeoutException {
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=name")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(responseMock);
when(responseMock.getEncoding()).thenReturn("utf8");
when(responseMock.getContentAsString()).thenReturn("{\"successful\": true, \"name\": \"hugo\"}");
Name name = subject.getName();
assertEquals("hugo", name.getName());
verify(httpClientMock, times(1)).newRequest("http://123.456.789.123/json?cmd=name");
}
@Test
public void shouldSetNewName() throws InterruptedException, ExecutionException, TimeoutException {
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=name&name=MyRobo")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(responseMock);
when(responseMock.getEncoding()).thenReturn("utf8");
when(responseMock.getContentAsString()).thenReturn("{\"successful\": true, \"name\": \"MyRobo\"}");
Name name = subject.setName("MyRobo");
assertEquals("MyRobo", name.getName());
verify(httpClientMock, times(1)).newRequest("http://123.456.789.123/json?cmd=name&name=MyRobo");
}
@Test
public void shouldListErrors() throws InterruptedException, ExecutionException, TimeoutException {
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=error")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(responseMock);
when(responseMock.getEncoding()).thenReturn("utf8");
when(responseMock.getContentAsString()).thenReturn(
"{\"errors\": [{\"error_code\": 33, \"error_message\": \"Grasi ist gekippt\", \"date\": \"04.05.2017\", \"time\": \"22:22:17\", \"unix\": 1493936537}, {\"error_code\": 15, \"error_message\": \"Grasi ist angehoben\", \"date\": \"02.05.2017\", \"time\": \"20:36:43\", \"unix\": 1493757403}, {\"error_code\": 33, \"error_message\": \"Grasi ist gekippt\", \"date\": \"26.04.2017\", \"time\": \"21:31:18\", \"unix\": 1493242278}, {\"error_code\": 13, \"error_message\": \"Kein Antrieb\", \"date\": \"21.04.2017\", \"time\": \"20:17:22\", \"unix\": 1492805842}, {\"error_code\": 10, \"error_message\": \"Grasi ist umgedreht\", \"date\": \"20.04.2017\", \"time\": \"20:14:37\", \"unix\": 1492719277}, {\"error_code\": 1, \"error_message\": \"Grasi hat Arbeitsbereich überschritten\", \"date\": \"12.04.2017\", \"time\": \"19:10:09\", \"unix\": 1492024209}, {\"error_code\": 33, \"error_message\": \"Grasi ist gekippt\", \"date\": \"10.04.2017\", \"time\": \"22:59:35\", \"unix\": 1491865175}, {\"error_code\": 1, \"error_message\": \"Grasi hat Arbeitsbereich überschritten\", \"date\": \"10.04.2017\", \"time\": \"21:21:55\", \"unix\": 1491859315}, {\"error_code\": 33, \"error_message\": \"Grasi ist gekippt\", \"date\": \"10.04.2017\", \"time\": \"20:26:13\", \"unix\": 1491855973}, {\"error_code\": 1, \"error_message\": \"Grasi hat Arbeitsbereich überschritten\", \"date\": \"09.04.2017\", \"time\": \"14:50:36\", \"unix\": 1491749436}, {\"error_code\": 33, \"error_message\": \"Grasi ist gekippt\", \"date\": \"09.04.2017\", \"time\": \"14:23:27\", \"unix\": 1491747807}], \"successful\": true}");
ErrorList list = subject.errorList();
assertEquals(11, list.getErrors().size());
verify(httpClientMock, times(1)).newRequest("http://123.456.789.123/json?cmd=error");
}
@Test
public void shouldRetrieveVersionInfo() throws InterruptedException, ExecutionException, TimeoutException {
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=version")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(responseMock);
when(responseMock.getEncoding()).thenReturn("utf8");
when(responseMock.getContentAsString()).thenReturn(
"{\"robonect\": {\"serial\": \"05D92D32-38355048-43203030\", \"version\": \"V0.9\", \"compiled\": \"2017-03-25 20:10:00\", \"comment\": \"V0.9c\"}, \"successful\": true}");
VersionInfo info = subject.getVersionInfo();
assertEquals("05D92D32-38355048-43203030", info.getRobonect().getSerial());
verify(httpClientMock, times(1)).newRequest("http://123.456.789.123/json?cmd=version");
}
@Test
public void shouldHandleProperEncoding() throws InterruptedException, ExecutionException, TimeoutException {
byte[] responseBytesISO88591 = "{\"successful\": true, \"name\": \"Mein Automower\", \"status\": {\"status\": 7, \"stopped\": true, \"duration\": 192, \"mode\": 1, \"battery\": 95, \"hours\": 41}, \"timer\": {\"status\": 2}, \"error\" : {\"error_code\": 15, \"error_message\": \"Utanför arbetsområdet\", \"date\": \"02.05.2017\", \"time\": \"20:36:43\", \"unix\": 1493757403}, \"wlan\": {\"signal\": -75}}"
.getBytes(StandardCharsets.ISO_8859_1);
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=status")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(responseMock);
when(responseMock.getEncoding()).thenReturn(null);
when(responseMock.getContent()).thenReturn(responseBytesISO88591);
MowerInfo info = subject.getMowerInfo();
assertEquals("Utanför arbetsområdet", info.getError().getErrorMessage());
verify(httpClientMock, times(1)).newRequest("http://123.456.789.123/json?cmd=status");
}
@Test(expected = RobonectCommunicationException.class)
public void shouldReceiveErrorAnswerOnInterruptedException()
throws InterruptedException, ExecutionException, TimeoutException {
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=status")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenThrow(new InterruptedException("Mock Interrupted Exception"));
MowerInfo answer = subject.getMowerInfo();
}
@Test(expected = RobonectCommunicationException.class)
public void shouldReceiveErrorAnswerOnExecutionException()
throws InterruptedException, ExecutionException, TimeoutException {
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=status")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenThrow(new ExecutionException(new Exception("Mock Exception")));
MowerInfo answer = subject.getMowerInfo();
}
@Test(expected = RobonectCommunicationException.class)
public void shouldReceiveErrorAnswerOnTimeoutException()
throws InterruptedException, ExecutionException, TimeoutException {
when(httpClientMock.newRequest("http://123.456.789.123/json?cmd=status")).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(30000L, TimeUnit.MILLISECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenThrow(new TimeoutException("Mock Timeout Exception"));
MowerInfo answer = subject.getMowerInfo();
}
}

View File

@@ -0,0 +1,333 @@
/**
* 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.robonect.internal.handler;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.eclipse.jetty.client.HttpClient;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.openhab.binding.robonect.internal.RobonectBindingConstants;
import org.openhab.binding.robonect.internal.RobonectClient;
import org.openhab.binding.robonect.internal.model.ErrorEntry;
import org.openhab.binding.robonect.internal.model.ErrorList;
import org.openhab.binding.robonect.internal.model.MowerInfo;
import org.openhab.binding.robonect.internal.model.MowerMode;
import org.openhab.binding.robonect.internal.model.MowerStatus;
import org.openhab.binding.robonect.internal.model.NextTimer;
import org.openhab.binding.robonect.internal.model.Status;
import org.openhab.binding.robonect.internal.model.Timer;
import org.openhab.binding.robonect.internal.model.Wlan;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The goal of this class is to test RobonectHandler in isolation.
*
* @author Marco Meyer - Initial contribution
*/
public class RobonectHandlerTest {
private RobonectHandler subject;
@Mock
private Thing robonectThingMock;
@Mock
private RobonectClient robonectClientMock;
@Mock
private ThingHandlerCallback callbackMock;
@Mock
private HttpClient httpClientMock;
@Mock
private TimeZoneProvider timezoneProvider;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
subject = new RobonectHandler(robonectThingMock, httpClientMock, timezoneProvider);
subject.setCallback(callbackMock);
subject.setRobonectClient(robonectClientMock);
Mockito.when(timezoneProvider.getTimeZone()).thenReturn(ZoneId.of("Europe/Berlin"));
}
@Test
public void shouldUpdateNextTimerChannelWithDateTimeState() throws InterruptedException {
ArgumentCaptor<State> stateCaptor = ArgumentCaptor.forClass(State.class);
// given
MowerInfo mowerInfo = createSuccessfulMowerInfoResponse();
Timer timer = new Timer();
timer.setStatus(Timer.TimerMode.ACTIVE);
NextTimer nextTimer = new NextTimer();
nextTimer.setDate("01.05.2017");
nextTimer.setTime("19:00:00");
nextTimer.setUnix("1493665200");
timer.setNext(nextTimer);
// when
when(robonectClientMock.getMowerInfo()).thenReturn(mowerInfo);
when(robonectThingMock.getUID()).thenReturn(new ThingUID("1:2:3"));
subject.handleCommand(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_TIMER_NEXT_TIMER),
RefreshType.REFRESH);
// then
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_TIMER_NEXT_TIMER)),
stateCaptor.capture());
State value = stateCaptor.getValue();
assertTrue(value instanceof DateTimeType);
ZonedDateTime zdt = ((DateTimeType) value).getZonedDateTime();
assertEquals(1, zdt.getDayOfMonth());
assertEquals(2017, zdt.getYear());
assertEquals(Month.MAY, zdt.getMonth());
assertEquals(19, zdt.getHour());
assertEquals(0, zdt.getMinute());
assertEquals(0, zdt.getSecond());
}
@Test
public void shouldUpdateErrorChannelsIfErrorStatusReturned() throws InterruptedException {
ArgumentCaptor<State> errorCodeCaptor = ArgumentCaptor.forClass(State.class);
ArgumentCaptor<State> errorMessageCaptor = ArgumentCaptor.forClass(State.class);
ArgumentCaptor<State> errorDateCaptor = ArgumentCaptor.forClass(State.class);
// given
MowerInfo mowerInfo = createSuccessfulMowerInfoResponse();
ErrorEntry error = new ErrorEntry();
error.setDate("01.05.2017");
error.setTime("19:00:00");
error.setUnix("1493665200");
error.setErrorCode(Integer.valueOf(22));
error.setErrorMessage("Dummy Message");
mowerInfo.getStatus().setStatus(MowerStatus.ERROR_STATUS);
mowerInfo.setError(error);
ErrorList errorList = new ErrorList();
errorList.setSuccessful(true);
// when
when(robonectClientMock.getMowerInfo()).thenReturn(mowerInfo);
when(robonectClientMock.errorList()).thenReturn(errorList);
when(robonectThingMock.getUID()).thenReturn(new ThingUID("1:2:3"));
subject.handleCommand(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_STATUS),
RefreshType.REFRESH);
// then
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_ERROR_CODE)),
errorCodeCaptor.capture());
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_ERROR_MESSAGE)),
errorMessageCaptor.capture());
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_ERROR_DATE)),
errorDateCaptor.capture());
State errorDate = errorDateCaptor.getValue();
assertTrue(errorDate instanceof DateTimeType);
ZonedDateTime zdt = ((DateTimeType) errorDate).getZonedDateTime();
assertEquals(1, zdt.getDayOfMonth());
assertEquals(2017, zdt.getYear());
assertEquals(Month.MAY, zdt.getMonth());
assertEquals(19, zdt.getHour());
assertEquals(0, zdt.getMinute());
assertEquals(0, zdt.getSecond());
State errorMessage = errorMessageCaptor.getValue();
assertTrue(errorMessage instanceof StringType);
StringType msgStringType = (StringType) errorMessage;
assertEquals("Dummy Message", msgStringType.toFullString());
State errorCode = errorCodeCaptor.getValue();
assertTrue(errorCode instanceof DecimalType);
DecimalType codeDecimaltype = (DecimalType) errorCode;
assertEquals(22, codeDecimaltype.intValue());
}
@Test
public void shouldResetErrorStateIfNoErrorInStatusUpdate() throws InterruptedException {
ArgumentCaptor<State> errorCodeCaptor = ArgumentCaptor.forClass(State.class);
ArgumentCaptor<State> errorMessageCaptor = ArgumentCaptor.forClass(State.class);
ArgumentCaptor<State> errorDateCaptor = ArgumentCaptor.forClass(State.class);
// given
MowerInfo mowerInfo = createSuccessfulMowerInfoResponse();
mowerInfo.getStatus().setStatus(MowerStatus.MOWING);
mowerInfo.setError(null);
// when
when(robonectClientMock.getMowerInfo()).thenReturn(mowerInfo);
when(robonectThingMock.getUID()).thenReturn(new ThingUID("1:2:3"));
subject.handleCommand(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_STATUS),
RefreshType.REFRESH);
// then
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_ERROR_CODE)),
errorCodeCaptor.capture());
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_ERROR_MESSAGE)),
errorMessageCaptor.capture());
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_ERROR_DATE)),
errorDateCaptor.capture());
assertEquals(errorCodeCaptor.getValue(), UnDefType.UNDEF);
assertEquals(errorMessageCaptor.getValue(), UnDefType.UNDEF);
assertEquals(errorDateCaptor.getValue(), UnDefType.UNDEF);
}
@Test
public void shouldUpdateNumericStateOnMowerStatusRefresh() throws InterruptedException {
ArgumentCaptor<State> stateCaptor = ArgumentCaptor.forClass(State.class);
// given
MowerInfo mowerInfo = createSuccessfulMowerInfoResponse();
mowerInfo.getStatus().setStatus(MowerStatus.MOWING);
// when
when(robonectClientMock.getMowerInfo()).thenReturn(mowerInfo);
when(robonectThingMock.getUID()).thenReturn(new ThingUID("1:2:3"));
subject.handleCommand(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_STATUS),
RefreshType.REFRESH);
// then
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_STATUS)),
stateCaptor.capture());
State value = stateCaptor.getValue();
assertTrue(value instanceof DecimalType);
DecimalType status = (DecimalType) value;
assertEquals(MowerStatus.MOWING.getStatusCode(), status.intValue());
}
@Test
public void shouldUpdateAllChannels() {
ArgumentCaptor<State> stateCaptorName = ArgumentCaptor.forClass(State.class);
ArgumentCaptor<State> stateCaptorBattery = ArgumentCaptor.forClass(State.class);
ArgumentCaptor<State> stateCaptorStatus = ArgumentCaptor.forClass(State.class);
ArgumentCaptor<State> stateCaptorDuration = ArgumentCaptor.forClass(State.class);
ArgumentCaptor<State> stateCaptorHours = ArgumentCaptor.forClass(State.class);
ArgumentCaptor<State> stateCaptorMode = ArgumentCaptor.forClass(State.class);
ArgumentCaptor<State> stateCaptorStarted = ArgumentCaptor.forClass(State.class);
ArgumentCaptor<State> stateCaptorWlan = ArgumentCaptor.forClass(State.class);
// given
MowerInfo mowerInfo = createSuccessfulMowerInfoResponse();
ErrorList errorList = new ErrorList();
errorList.setSuccessful(true);
// when
when(robonectClientMock.getMowerInfo()).thenReturn(mowerInfo);
when(robonectClientMock.errorList()).thenReturn(errorList);
when(robonectThingMock.getUID()).thenReturn(new ThingUID("1:2:3"));
subject.handleCommand(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_STATUS),
RefreshType.REFRESH);
// then
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_MOWER_NAME)),
stateCaptorName.capture());
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_STATUS_BATTERY)),
stateCaptorBattery.capture());
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_STATUS)),
stateCaptorStatus.capture());
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_STATUS_DURATION)),
stateCaptorDuration.capture());
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_STATUS_HOURS)),
stateCaptorHours.capture());
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_STATUS_MODE)),
stateCaptorMode.capture());
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_MOWER_START)),
stateCaptorStarted.capture());
verify(callbackMock, times(1)).stateUpdated(
eq(new ChannelUID(new ThingUID("1:2:3"), RobonectBindingConstants.CHANNEL_WLAN_SIGNAL)),
stateCaptorWlan.capture());
assertEquals("Mowy", stateCaptorName.getValue().toFullString());
assertEquals(99, ((DecimalType) stateCaptorBattery.getValue()).intValue());
assertEquals(4, ((DecimalType) stateCaptorStatus.getValue()).intValue());
assertEquals(55, ((QuantityType<?>) stateCaptorDuration.getValue()).intValue());
assertEquals(22, ((QuantityType<?>) stateCaptorHours.getValue()).intValue());
assertEquals(MowerMode.AUTO.name(), stateCaptorMode.getValue().toFullString());
assertEquals(OnOffType.ON, stateCaptorStarted.getValue());
assertEquals(-88, ((DecimalType) stateCaptorWlan.getValue()).intValue());
}
private MowerInfo createSuccessfulMowerInfoResponse() {
MowerInfo mowerInfo = new MowerInfo();
Timer timer = new Timer();
timer.setStatus(Timer.TimerMode.ACTIVE);
NextTimer nextTimer = new NextTimer();
nextTimer.setDate("01.05.2017");
nextTimer.setTime("19:00:00");
nextTimer.setUnix("1493665200");
timer.setNext(nextTimer);
mowerInfo.setTimer(timer);
Status status = new Status();
status.setBattery(99);
status.setDuration(55);
status.setHours(22);
status.setMode(MowerMode.AUTO);
status.setStatus(MowerStatus.CHARGING);
mowerInfo.setStatus(status);
mowerInfo.setName("Mowy");
Wlan wlan = new Wlan();
wlan.setSignal(-88);
mowerInfo.setWlan(wlan);
mowerInfo.setSuccessful(true);
mowerInfo.getStatus().setStopped(false);
return mowerInfo;
}
}

View File

@@ -0,0 +1,201 @@
/**
* 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.robonect.internal.model;
import static org.junit.Assert.*;
import java.nio.charset.StandardCharsets;
import org.junit.Before;
import org.junit.Test;
/**
* The goal of this class is to test the model parser to make sure the structures
* returned from the module can be handled.
*
* @author Marco Meyer - Initial contribution
*/
public class ModelParserTest {
private ModelParser subject;
@Before
public void setUp() {
subject = new ModelParser();
}
@Test
public void shouldParseSimpleSuccessModel() {
String correctModel = "{\"successful\": true}";
RobonectAnswer answer = subject.parse(correctModel, RobonectAnswer.class);
assertTrue(answer.isSuccessful());
assertNull(answer.getErrorMessage());
assertNull(answer.getErrorCode());
}
@Test
public void shouldParseErrorResponseOnAllResponseTypes() {
String correctModel = "{\"successful\": false, \"error_code\": 7, \"error_message\": \"Automower already stopped\"}";
RobonectAnswer answer = subject.parse(correctModel, RobonectAnswer.class);
assertFalse(answer.isSuccessful());
assertEquals(new Integer(7), answer.getErrorCode());
assertEquals("Automower already stopped", answer.getErrorMessage());
MowerInfo info = subject.parse(correctModel, MowerInfo.class);
assertFalse(info.isSuccessful());
assertEquals(new Integer(7), info.getErrorCode());
assertEquals("Automower already stopped", info.getErrorMessage());
}
@Test
public void shouldParseCorrectStatusModel() {
String correctModel = "{\"successful\": true, \"name\": \"Mein Automower\", \"status\": {\"status\": 17, \"stopped\": false, \"duration\": 4359, \"mode\": 0, \"battery\": 100, \"hours\": 29}, \"timer\": {\"status\": 2, \"next\": {\"date\": \"01.05.2017\", \"time\": \"19:00:00\", \"unix\": 1493665200}}, \"wlan\": {\"signal\": -76}}";
MowerInfo mowerInfo = subject.parse(correctModel, MowerInfo.class);
assertTrue(mowerInfo.isSuccessful());
assertEquals("Mein Automower", mowerInfo.getName());
assertEquals(MowerStatus.SLEEPING, mowerInfo.getStatus().getStatus());
assertFalse(mowerInfo.getStatus().isStopped());
assertEquals(4359, mowerInfo.getStatus().getDuration());
assertEquals(MowerMode.AUTO, mowerInfo.getStatus().getMode());
assertEquals(100, mowerInfo.getStatus().getBattery());
assertEquals(29, mowerInfo.getStatus().getHours());
assertEquals(Timer.TimerMode.STANDBY, mowerInfo.getTimer().getStatus());
assertEquals("01.05.2017", mowerInfo.getTimer().getNext().getDate());
assertEquals("19:00:00", mowerInfo.getTimer().getNext().getTime());
assertEquals("1493665200", mowerInfo.getTimer().getNext().getUnix());
assertEquals(-76, mowerInfo.getWlan().getSignal());
assertNull(mowerInfo.getError());
}
@Test
public void shouldParseCorrectStatusModelWithHealth() {
String correctModel = "{ \"successful\": true, \"name\": \"Rosenlund Automower\", \"status\": { \"status\": 4, \"stopped\": false, \"duration\": 47493, \"mode\": 0, \"battery\": 20, \"hours\": 991 }, \"timer\": { \"status\": 2, \"next\": { \"date\": \"30.07.2017\", \"time\": \"13:00:00\", \"unix\": 1501419600 } }, \"wlan\": { \"signal\": -66 }, \"health\": { \"temperature\": 28, \"humidity\": 32 } }";
MowerInfo mowerInfo = subject.parse(correctModel, MowerInfo.class);
assertTrue(mowerInfo.isSuccessful());
assertEquals("Rosenlund Automower", mowerInfo.getName());
assertEquals(MowerStatus.CHARGING, mowerInfo.getStatus().getStatus());
assertFalse(mowerInfo.getStatus().isStopped());
assertEquals(47493, mowerInfo.getStatus().getDuration());
assertEquals(MowerMode.AUTO, mowerInfo.getStatus().getMode());
assertEquals(20, mowerInfo.getStatus().getBattery());
assertEquals(991, mowerInfo.getStatus().getHours());
assertEquals(Timer.TimerMode.STANDBY, mowerInfo.getTimer().getStatus());
assertEquals("30.07.2017", mowerInfo.getTimer().getNext().getDate());
assertEquals("13:00:00", mowerInfo.getTimer().getNext().getTime());
assertEquals("1501419600", mowerInfo.getTimer().getNext().getUnix());
assertEquals(-66, mowerInfo.getWlan().getSignal());
assertNull(mowerInfo.getError());
assertNotNull(mowerInfo.getHealth());
assertEquals(28, mowerInfo.getHealth().getTemperature());
assertEquals(32, mowerInfo.getHealth().getHumidity());
// "health": { "temperature": 28, "humidity": 32 }
}
@Test
public void shouldParseISOEncodedStatusModel() {
byte[] responseBytesISO88591 = "{\"successful\": true, \"name\": \"Mein Automower\", \"status\": {\"status\": 7, \"stopped\": true, \"duration\": 192, \"mode\": 1, \"battery\": 95, \"hours\": 41}, \"timer\": {\"status\": 2}, \"error\" : {\"error_code\": 15, \"error_message\": \"Utanför arbetsområdet\", \"date\": \"02.05.2017\", \"time\": \"20:36:43\", \"unix\": 1493757403}, \"wlan\": {\"signal\": -75}}"
.getBytes(StandardCharsets.ISO_8859_1);
MowerInfo mowerInfo = subject.parse(new String(responseBytesISO88591, StandardCharsets.ISO_8859_1),
MowerInfo.class);
assertEquals("Utanför arbetsområdet", mowerInfo.getError().getErrorMessage());
}
@Test
public void shouldParseCorrectStatusModelWithErrorCode() {
String correctModel = "{\"successful\": true, \"name\": \"Grasi\", \"status\": {\"status\": 7, \"stopped\": true, \"duration\": 423, \"mode\": 0, \"battery\": 83, \"hours\": 55}, \"timer\": {\"status\": 2, \"next\": {\"date\": \"15.05.2017\", \"time\": \"19:00:00\", \"unix\": 1494874800}}, \"wlan\": {\"signal\": -76}, \"error\": {\"error_code\": 9, \"error_message\": \"Grasi ist eingeklemmt\", \"date\": \"13.05.2017\", \"time\": \"23:00:22\", \"unix\": 1494716422}}";
MowerInfo mowerInfo = subject.parse(correctModel, MowerInfo.class);
assertTrue(mowerInfo.isSuccessful());
assertEquals("Grasi", mowerInfo.getName());
assertEquals(MowerStatus.ERROR_STATUS, mowerInfo.getStatus().getStatus());
assertTrue(mowerInfo.getStatus().isStopped());
assertEquals(9, mowerInfo.getError().getErrorCode().intValue());
assertEquals("Grasi ist eingeklemmt", mowerInfo.getError().getErrorMessage());
}
@Test
public void shouldParseCorrectStatusModelMowing() {
String correctModel = "{\"successful\": true, \"name\": \"Mein Automower\", \"status\": {\"status\": 2, \"stopped\": false, \"duration\": 192, \"mode\": 1, \"battery\": 95, \"hours\": 41}, \"timer\": {\"status\": 2}, \"wlan\": {\"signal\": -75}}";
MowerInfo mowerInfo = subject.parse(correctModel, MowerInfo.class);
assertTrue(mowerInfo.isSuccessful());
assertEquals("Mein Automower", mowerInfo.getName());
assertEquals(MowerStatus.MOWING, mowerInfo.getStatus().getStatus());
assertFalse(mowerInfo.getStatus().isStopped());
assertEquals(MowerMode.MANUAL, mowerInfo.getStatus().getMode());
}
@Test
public void shouldParseCorrectErrorModelInErrorState() {
String correctModel = "{\"successful\": true, \"name\": \"Mein Automower\", \"status\": {\"status\": 7, \"stopped\": true, \"duration\": 192, \"mode\": 1, \"battery\": 95, \"hours\": 41}, \"timer\": {\"status\": 2}, \"error\" : {\"error_code\": 15, \"error_message\": \"Mein Automower ist angehoben\", \"date\": \"02.05.2017\", \"time\": \"20:36:43\", \"unix\": 1493757403}, \"wlan\": {\"signal\": -75}}";
MowerInfo mowerInfo = subject.parse(correctModel, MowerInfo.class);
assertTrue(mowerInfo.isSuccessful());
assertEquals("Mein Automower", mowerInfo.getName());
assertEquals(MowerStatus.ERROR_STATUS, mowerInfo.getStatus().getStatus());
assertTrue(mowerInfo.getStatus().isStopped());
assertNotNull(mowerInfo.getError());
assertEquals("Mein Automower ist angehoben", mowerInfo.getError().getErrorMessage());
assertEquals(new Integer(15), mowerInfo.getError().getErrorCode());
assertEquals("02.05.2017", mowerInfo.getError().getDate());
assertEquals("20:36:43", mowerInfo.getError().getTime());
assertEquals("1493757403", mowerInfo.getError().getUnix());
}
@Test
public void shouldParseErrorsList() {
String errorsListResponse = "{\"errors\": [{\"error_code\": 15, \"error_message\": \"Grasi ist angehoben\", \"date\": \"02.05.2017\", \"time\": \"20:36:43\", \"unix\": 1493757403}, {\"error_code\": 33, \"error_message\": \"Grasi ist gekippt\", \"date\": \"26.04.2017\", \"time\": \"21:31:18\", \"unix\": 1493242278}, {\"error_code\": 13, \"error_message\": \"Kein Antrieb\", \"date\": \"21.04.2017\", \"time\": \"20:17:22\", \"unix\": 1492805842}, {\"error_code\": 10, \"error_message\": \"Grasi ist umgedreht\", \"date\": \"20.04.2017\", \"time\": \"20:14:37\", \"unix\": 1492719277}, {\"error_code\": 1, \"error_message\": \"Grasi hat Arbeitsbereich überschritten\", \"date\": \"12.04.2017\", \"time\": \"19:10:09\", \"unix\": 1492024209}, {\"error_code\": 33, \"error_message\": \"Grasi ist gekippt\", \"date\": \"10.04.2017\", \"time\": \"22:59:35\", \"unix\": 1491865175}, {\"error_code\": 1, \"error_message\": \"Grasi hat Arbeitsbereich überschritten\", \"date\": \"10.04.2017\", \"time\": \"21:21:55\", \"unix\": 1491859315}, {\"error_code\": 33, \"error_message\": \"Grasi ist gekippt\", \"date\": \"10.04.2017\", \"time\": \"20:26:13\", \"unix\": 1491855973}, {\"error_code\": 1, \"error_message\": \"Grasi hat Arbeitsbereich überschritten\", \"date\": \"09.04.2017\", \"time\": \"14:50:36\", \"unix\": 1491749436}, {\"error_code\": 33, \"error_message\": \"Grasi ist gekippt\", \"date\": \"09.04.2017\", \"time\": \"14:23:27\", \"unix\": 1491747807}], \"successful\": true}";
ErrorList errorList = subject.parse(errorsListResponse, ErrorList.class);
assertTrue(errorList.isSuccessful());
assertEquals(10, errorList.getErrors().size());
assertEquals(new Integer(15), errorList.getErrors().get(0).getErrorCode());
assertEquals("Grasi ist angehoben", errorList.getErrors().get(0).getErrorMessage());
assertEquals("02.05.2017", errorList.getErrors().get(0).getDate());
assertEquals("20:36:43", errorList.getErrors().get(0).getTime());
assertEquals("1493757403", errorList.getErrors().get(0).getUnix());
}
@Test
public void shouldParseName() {
String nameResponse = "{\"name\": \"Grasi\", \"successful\": true}";
Name name = subject.parse(nameResponse, Name.class);
assertTrue(name.isSuccessful());
assertEquals("Grasi", name.getName());
}
@Test
public void shouldParseVersionInfo() {
String versionResponse = "{\"robonect\": {\"serial\": \"05D92D32-38355048-43203030\", \"version\": \"V0.9\", \"compiled\": \"2017-03-25 20:10:00\", \"comment\": \"V0.9c\"}, \"successful\": true}";
VersionInfo versionInfo = subject.parse(versionResponse, VersionInfo.class);
assertTrue(versionInfo.isSuccessful());
assertEquals("05D92D32-38355048-43203030", versionInfo.getRobonect().getSerial());
assertEquals("V0.9", versionInfo.getRobonect().getVersion());
assertEquals("2017-03-25 20:10:00", versionInfo.getRobonect().getCompiled());
assertEquals("V0.9c", versionInfo.getRobonect().getComment());
}
@Test
public void shouldParseVersionInfoV1betaToNA() {
String versionResponse = "{\n" + "mower: {\n" + "hardware: {\n" + "serial: 170602001,\n"
+ "production: \"2017-02-07 15:12:00\"\n" + "},\n" + "msw: {\n" + "title: \"420\",\n"
+ "version: \"7.10.00\",\n" + "compiled: \"2016-11-29 08:44:06\"\n" + "},\n" + "sub: {\n"
+ "version: \"6.01.00\"\n" + "}\n" + "},\n" + "serial: \"05D80037-39355548-43163930\",\n"
+ "bootloader: {\n" + "version: \"V0.4\",\n" + "compiled: \"2016-10-22 01:12:00\",\n"
+ "comment: \"\"\n" + "},\n" + "wlan: {\n" + "at-version: \"V1.4.0\",\n" + "sdk-version: \"V2.1.0\"\n"
+ "},\n" + "application: {\n" + "version: \"V1.0\",\n" + "compiled: \"2018-03-12 21:01:00\",\n"
+ "comment: \"Release V1.0 Beta2\"\n" + "},\n" + "successful: true\n" + "}";
VersionInfo versionInfo = subject.parse(versionResponse, VersionInfo.class);
assertTrue(versionInfo.isSuccessful());
assertEquals("n/a", versionInfo.getRobonect().getSerial());
assertEquals("n/a", versionInfo.getRobonect().getVersion());
assertEquals("n/a", versionInfo.getRobonect().getCompiled());
assertEquals("n/a", versionInfo.getRobonect().getComment());
}
}