[boschindego] Add new channels (#13040)

Fixes #12938

Fixes #13017

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen 2022-07-02 12:31:51 +02:00 committed by GitHub
parent 53d72dcecb
commit 5e4ca2568c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 410 additions and 46 deletions

View File

@ -9,24 +9,30 @@ His [Java Library](https://github.com/zazaz-de/iot-device-bosch-indego-controlle
Currently the binding supports ***indego*** mowers as a thing type with these configuration parameters:
| Parameter | Description | Default |
|--------------------|-----------------------------------------------------------------|---------|
|-----------------------|-------------------------------------------------------------------------|---------|
| username | Username for the Bosch Indego account | |
| password | Password for the Bosch Indego account | |
| refresh | The number of seconds between refreshing device state | 180 |
| cuttingTimeRefresh | The number of minutes between refreshing last/next cutting time | 60 |
| cuttingTimeMapRefresh | The number of minutes between refreshing last/next cutting time and map | 60 |
## Channels
| Channel | Item Type | Description |
|--------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------|
| state | Number | You can send commands to this channel to control the mower and read the simplified state from it (1=mow, 2=return to dock, 3=pause) |
| errorcode | Number | Error code of the mower (0=no error, readonly) |
| statecode | Number | Detailed state of the mower (readonly) |
| textualstate | String | State as a text. (readonly) |
| ready | Number | Shows if the mower is ready to mow (1=ready, 0=not ready, readonly) |
| mowed | Dimmer | Cut grass in percent (readonly) |
| lastCutting | DateTime | Last cutting time (readonly) |
| nextCutting | DateTime | Next scheduled cutting time (readonly) |
| Channel | Item Type | Description | Writeable |
|--------------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------|-----------|
| state | Number | You can send commands to this channel to control the mower and read the simplified state from it (1=mow, 2=return to dock, 3=pause) | Yes |
| errorcode | Number | Error code of the mower (0=no error) | |
| statecode | Number | Detailed state of the mower | |
| textualstate | String | State as a text. | |
| ready | Number | Shows if the mower is ready to mow (1=ready, 0=not ready) | |
| mowed | Dimmer | Cut grass in percent | |
| lastCutting | DateTime | Last cutting time | |
| nextCutting | DateTime | Next scheduled cutting time | |
| batteryVoltage | Number:ElectricPotential | Battery voltage reported by the device | |
| batteryLevel | Number | Battery level as a percentage (0-100%) | |
| lowBattery | Switch | Low battery warning with possible values on (low battery) and off (battery ok) | |
| batteryTemperature | Number:Temperature | Battery temperature reported by the device | |
| gardenSize | Number:Area | Garden size mapped by the device | |
| gardenMap | Image | Garden map mapped by the device | |
### State Codes
@ -81,6 +87,12 @@ Number Indego_Ready { channel="boschindego:indego:lawnmower:ready" }
Dimmer Indego_Mowed { channel="boschindego:indego:lawnmower:mowed" }
DateTime Indego_LastCutting { channel="boschindego:indego:lawnmower:lastCutting" }
DateTime Indego_NextCutting { channel="boschindego:indego:lawnmower:nextCutting" }
Number:ElectricPotential Indego_BatteryVoltage { channel="boschindego:indego:lawnmower:batteryVoltage" }
Number Indego_BatteryLevel { channel="boschindego:indego:lawnmower:batteryLevel" }
Switch Indego_LowBattery { channel="boschindego:indego:lawnmower:lowBattery" }
Number:Temperature Indego_BatteryTemperature { channel="boschindego:indego:lawnmower:batteryTemperature" }
Number:Area Indego_GardenSize { channel="boschindego:indego:lawnmower:gardenSize" }
Image Indego_GardenMap { channel="boschindego:indego:lawnmower:gardenMap" }
```
### `indego.sitemap` File

View File

@ -40,6 +40,12 @@ public class BoschIndegoBindingConstants {
public static final String READY = "ready";
public static final String LAST_CUTTING = "lastCutting";
public static final String NEXT_CUTTING = "nextCutting";
public static final String BATTERY_VOLTAGE = "batteryVoltage";
public static final String BATTERY_LEVEL = "batteryLevel";
public static final String LOW_BATTERY = "lowBattery";
public static final String BATTERY_TEMPERATURE = "batteryTemperature";
public static final String GARDEN_SIZE = "gardenSize";
public static final String GARDEN_MAP = "gardenMap";
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_INDEGO);
}

View File

@ -38,12 +38,15 @@ import org.openhab.binding.boschindego.internal.dto.response.AuthenticationRespo
import org.openhab.binding.boschindego.internal.dto.response.DeviceCalendarResponse;
import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse;
import org.openhab.binding.boschindego.internal.dto.response.LocationWeatherResponse;
import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse;
import org.openhab.binding.boschindego.internal.dto.response.PredictiveLastCuttingResponse;
import org.openhab.binding.boschindego.internal.dto.response.PredictiveNextCuttingResponse;
import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoUnreachableException;
import org.openhab.core.library.types.RawType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -216,6 +219,9 @@ public class IndegoController {
// This will currently not happen because "WWW-Authenticate" header is missing; see below.
throw new IndegoAuthenticationException("Context rejected");
}
if (status == HttpStatus.GATEWAY_TIMEOUT_504) {
throw new IndegoUnreachableException("Gateway timeout");
}
if (!HttpStatus.isSuccess(status)) {
throw new IndegoException("The request failed with error: " + status);
}
@ -256,6 +262,93 @@ public class IndegoController {
}
}
/**
* Wraps {@link #getRawRequest(String)} into an authenticated session.
*
* @param path the relative path to which the request should be sent
* @return the raw data from the response
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
private RawType getRawRequestWithAuthentication(String path) throws IndegoAuthenticationException, IndegoException {
if (!session.isValid()) {
authenticate();
}
try {
logger.debug("Session {} valid, skipping authentication", session);
return getRawRequest(path);
} catch (IndegoAuthenticationException e) {
if (logger.isTraceEnabled()) {
logger.trace("Context rejected", e);
} else {
logger.debug("Context rejected: {}", e.getMessage());
}
session.invalidate();
authenticate();
return getRawRequest(path);
}
}
/**
* Sends a GET request to the server and returns the raw response.
*
* @param path the relative path to which the request should be sent
* @return the raw data from the response
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
private RawType getRawRequest(String path) throws IndegoAuthenticationException, IndegoException {
try {
Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET).header(CONTEXT_HEADER_NAME,
session.getContextId());
if (logger.isTraceEnabled()) {
logger.trace("GET request for {}", BASE_URL + path);
}
ContentResponse response = sendRequest(request);
int status = response.getStatus();
if (status == HttpStatus.UNAUTHORIZED_401) {
// This will currently not happen because "WWW-Authenticate" header is missing; see below.
throw new IndegoAuthenticationException("Context rejected");
}
if (!HttpStatus.isSuccess(status)) {
throw new IndegoException("The request failed with error: " + status);
}
byte[] data = response.getContent();
if (data == null) {
throw new IndegoInvalidResponseException("No data returned");
}
String contentType = response.getMediaType();
if (contentType == null || contentType.isEmpty()) {
throw new IndegoInvalidResponseException("No content-type returned");
}
logger.debug("Media download response: type {}, length {}", contentType, data.length);
return new RawType(data, contentType);
} catch (JsonParseException e) {
throw new IndegoInvalidResponseException("Error parsing response", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IndegoException(e);
} catch (TimeoutException e) {
throw new IndegoException(e);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause != null && cause instanceof HttpResponseException) {
Response response = ((HttpResponseException) cause).getResponse();
if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
/*
* When contextId is not valid, the service will respond with HTTP code 401 without
* any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
* HttpResponseException. We need to handle this in order to attempt
* reauthentication.
*/
throw new IndegoAuthenticationException("Context rejected", e);
}
}
throw new IndegoException(e);
}
}
/**
* Wraps {@link #putRequest(String, Object)} into an authenticated session.
*
@ -381,6 +474,30 @@ public class IndegoController {
DeviceStateResponse.class);
}
/**
* Queries the device operating data from the server.
* Server will request this directly from the device, so operation might be slow.
*
* @return the device state
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public OperatingDataResponse getOperatingData() throws IndegoAuthenticationException, IndegoException {
return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/operatingData",
OperatingDataResponse.class);
}
/**
* Queries the map generated by the device from the server.
*
* @return the garden map
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public RawType getMap() throws IndegoAuthenticationException, IndegoException {
return getRawRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/map");
}
/**
* Queries the calendar.
*

View File

@ -25,5 +25,5 @@ public class BoschIndegoConfiguration {
public @Nullable String username;
public @Nullable String password;
public long refresh = 180;
public long cuttingTimeRefresh = 60;
public long cuttingTimeMapRefresh = 60;
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2022 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.boschindego.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* Battery data.
*
* @author Jacob Laursen - Initial contribution
*/
public class Battery {
public double voltage;
public int cycles;
public double discharge;
@SerializedName("ambient_temp")
public int ambientTemperature;
@SerializedName("battery_temp")
public int batteryTemperature;
public int percent;
}

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2022 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.boschindego.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* Garden data.
*
* @author Jacob Laursen - Initial contribution
*/
public class Garden {
public long id;
public String name;
@SerializedName("signal_id")
public byte signalId;
public int size;
@SerializedName("inner_bounds")
public int innerBounds;
public int cuts;
public int runtime;
public int charge;
public int bumps;
public int stops;
@SerializedName("last_mow")
public int lastMow;
@SerializedName("map_cell_size")
public int mapCellSize;
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 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.boschindego.internal.dto.response;
import org.openhab.binding.boschindego.internal.dto.Battery;
import org.openhab.binding.boschindego.internal.dto.Garden;
import org.openhab.binding.boschindego.internal.dto.response.runtime.DeviceStateRuntimes;
/**
* Response for operating data.
*
* @author Jacob Laursen - Initial contribution
*/
public class OperatingDataResponse {
public DeviceStateRuntimes runtime;
public Battery battery;
public Garden garden;
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2022 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.boschindego.internal.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link IndegoUnreachableException} is thrown on gateway timeout, which
* means that Bosch services cannot connect to the device.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class IndegoUnreachableException extends IndegoException {
private static final long serialVersionUID = -7952585411438042139L;
public IndegoUnreachableException(String message) {
super(message);
}
public IndegoUnreachableException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -28,13 +28,18 @@ import org.openhab.binding.boschindego.internal.IndegoController;
import org.openhab.binding.boschindego.internal.config.BoschIndegoConfiguration;
import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse;
import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse;
import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
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.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@ -63,7 +68,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
private @NonNullByDefault({}) IndegoController controller;
private @Nullable ScheduledFuture<?> statePollFuture;
private @Nullable ScheduledFuture<?> cuttingTimePollFuture;
private @Nullable ScheduledFuture<?> cuttingTimeMapPollFuture;
private boolean propertiesInitialized;
private int previousStateCode;
@ -96,10 +101,11 @@ public class BoschIndegoHandler extends BaseThingHandler {
controller = new IndegoController(httpClient, username, password);
updateStatus(ThingStatus.UNKNOWN);
this.statePollFuture = scheduler.scheduleWithFixedDelay(this::refreshStateWithExceptionHandling, 0,
config.refresh, TimeUnit.SECONDS);
this.cuttingTimePollFuture = scheduler.scheduleWithFixedDelay(this::refreshCuttingTimesWithExceptionHandling, 0,
config.cuttingTimeRefresh, TimeUnit.MINUTES);
this.statePollFuture = scheduler.scheduleWithFixedDelay(this::refreshStateAndOperatingDataWithExceptionHandling,
0, config.refresh, TimeUnit.SECONDS);
this.cuttingTimeMapPollFuture = scheduler.scheduleWithFixedDelay(
this::refreshCuttingTimesAndMapWithExceptionHandling, 0, config.cuttingTimeMapRefresh,
TimeUnit.MINUTES);
}
@Override
@ -110,21 +116,21 @@ public class BoschIndegoHandler extends BaseThingHandler {
pollFuture.cancel(true);
}
this.statePollFuture = null;
pollFuture = this.cuttingTimePollFuture;
pollFuture = this.cuttingTimeMapPollFuture;
if (pollFuture != null) {
pollFuture.cancel(true);
}
this.cuttingTimePollFuture = null;
this.cuttingTimeMapPollFuture = null;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("handleCommand {} for channel {}", command, channelUID);
try {
if (command == RefreshType.REFRESH) {
handleRefreshCommand(channelUID.getId());
return;
}
if (command instanceof DecimalType && channelUID.getId().equals(STATE)) {
sendCommand(((DecimalType) command).intValue());
}
@ -144,11 +150,21 @@ public class BoschIndegoHandler extends BaseThingHandler {
case ERRORCODE:
case STATECODE:
case READY:
this.refreshState();
refreshState();
break;
case LAST_CUTTING:
case NEXT_CUTTING:
this.refreshCuttingTimes();
refreshCuttingTimes();
break;
case BATTERY_LEVEL:
case LOW_BATTERY:
case BATTERY_VOLTAGE:
case BATTERY_TEMPERATURE:
case GARDEN_SIZE:
refreshOperatingData();
break;
case GARDEN_MAP:
refreshMap();
break;
}
}
@ -178,14 +194,13 @@ public class BoschIndegoHandler extends BaseThingHandler {
logger.debug("Sending command {}", command);
updateState(TEXTUAL_STATE, UnDefType.UNDEF);
controller.sendCommand(command);
state = controller.getState();
updateStatus(ThingStatus.ONLINE);
updateState(state);
refreshState();
}
private void refreshStateWithExceptionHandling() {
private void refreshStateAndOperatingDataWithExceptionHandling() {
try {
refreshState();
refreshOperatingData();
} catch (IndegoAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.authentication-failure");
@ -201,7 +216,6 @@ public class BoschIndegoHandler extends BaseThingHandler {
}
DeviceStateResponse state = controller.getState();
updateStatus(ThingStatus.ONLINE);
updateState(state);
// When state code changed, refresh cutting times immediately.
@ -211,15 +225,9 @@ public class BoschIndegoHandler extends BaseThingHandler {
}
}
private void refreshCuttingTimesWithExceptionHandling() {
try {
refreshCuttingTimes();
} catch (IndegoAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.authentication-failure");
} catch (IndegoException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
private void refreshOperatingData() throws IndegoAuthenticationException, IndegoException {
updateOperatingData(controller.getOperatingData());
updateStatus(ThingStatus.ONLINE);
}
private void refreshCuttingTimes() throws IndegoAuthenticationException, IndegoException {
@ -244,6 +252,24 @@ public class BoschIndegoHandler extends BaseThingHandler {
}
}
private void refreshCuttingTimesAndMapWithExceptionHandling() {
try {
refreshCuttingTimes();
refreshMap();
} catch (IndegoAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.authentication-failure");
} catch (IndegoException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
private void refreshMap() throws IndegoAuthenticationException, IndegoException {
if (isLinked(GARDEN_MAP)) {
updateState(GARDEN_MAP, controller.getMap());
}
}
private void updateState(DeviceStateResponse state) {
DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
int status = getStatusFromCommand(deviceStatus.getAssociatedCommand());
@ -260,6 +286,14 @@ public class BoschIndegoHandler extends BaseThingHandler {
updateState(TEXTUAL_STATE, new StringType(deviceStatus.getMessage(translationProvider)));
}
private void updateOperatingData(OperatingDataResponse operatingData) {
updateState(BATTERY_VOLTAGE, new QuantityType<>(operatingData.battery.voltage, Units.VOLT));
updateState(BATTERY_LEVEL, new DecimalType(operatingData.battery.percent));
updateState(LOW_BATTERY, OnOffType.from(operatingData.battery.percent < 20));
updateState(BATTERY_TEMPERATURE, new QuantityType<>(operatingData.battery.batteryTemperature, SIUnits.CELSIUS));
updateState(GARDEN_SIZE, new QuantityType<>(operatingData.garden.size, SIUnits.SQUARE_METRE));
}
private boolean isReadyToMow(DeviceStatus deviceStatus, int error) {
return deviceStatus.isReadyToMow() && error == 0;
}

View File

@ -10,8 +10,8 @@ thing-type.boschindego.indego.description = Indego which supports the connect fe
# thing types config
thing-type.config.boschindego.indego.cuttingTimeRefresh.label = Cutting Time Refresh Interval
thing-type.config.boschindego.indego.cuttingTimeRefresh.description = The number of minutes between refreshing last/next cutting time.
thing-type.config.boschindego.indego.cuttingTimeMapRefresh.label = Cutting Time/Map Refresh Interval
thing-type.config.boschindego.indego.cuttingTimeMapRefresh.description = The number of minutes between refreshing last/next cutting time and map.
thing-type.config.boschindego.indego.password.label = Password
thing-type.config.boschindego.indego.password.description = Password for the Bosch Indego account.
thing-type.config.boschindego.indego.refresh.label = Refresh Interval
@ -21,8 +21,16 @@ thing-type.config.boschindego.indego.username.description = Username for the Bos
# channel types
channel-type.boschindego.batteryTemperature.label = Battery Temperature
channel-type.boschindego.batteryTemperature.description = Battery temperature reported by the device
channel-type.boschindego.batteryVoltage.label = Battery Voltage
channel-type.boschindego.batteryVoltage.description = Battery voltage reported by the device
channel-type.boschindego.errorcode.label = Error Code
channel-type.boschindego.errorcode.description = 0 = no error
channel-type.boschindego.gardenMap.label = Garden Map
channel-type.boschindego.gardenMap.description = Garden map mapped by the device
channel-type.boschindego.gardenSize.label = Garden Size
channel-type.boschindego.gardenSize.description = Garden size mapped by the device
channel-type.boschindego.lastCutting.label = Last Cutting
channel-type.boschindego.lastCutting.description = Last cutting time
channel-type.boschindego.mowed.label = Cut Grass
@ -44,6 +52,7 @@ channel-type.boschindego.textualstate.label = Textual State
# thing status descriptions
offline.comm-error.authentication-failure = The login credentials are wrong or another client is connected to your Indego account
offline.comm-error.unreachable = Device is unreachable
offline.conf-error.missing-password = Password missing
offline.conf-error.missing-username = Username missing

View File

@ -16,6 +16,12 @@
<channel id="ready" typeId="ready"/>
<channel id="lastCutting" typeId="lastCutting"/>
<channel id="nextCutting" typeId="nextCutting"/>
<channel id="batteryVoltage" typeId="batteryVoltage"/>
<channel id="batteryLevel" typeId="system.battery-level"/>
<channel id="lowBattery" typeId="system.low-battery"/>
<channel id="batteryTemperature" typeId="batteryTemperature"/>
<channel id="gardenSize" typeId="gardenSize"/>
<channel id="gardenMap" typeId="gardenMap"/>
</channels>
<config-description>
<parameter name="username" type="text" required="true">
@ -32,9 +38,9 @@
<description>The number of seconds between refreshing device state.</description>
<default>180</default>
</parameter>
<parameter name="cuttingTimeRefresh" type="integer" min="1">
<label>Cutting Time Refresh Interval</label>
<description>The number of minutes between refreshing last/next cutting time.</description>
<parameter name="cuttingTimeMapRefresh" type="integer" min="1">
<label>Cutting Time/Map Refresh Interval</label>
<description>The number of minutes between refreshing last/next cutting time and map.</description>
<advanced>true</advanced>
<default>60</default>
</parameter>
@ -132,5 +138,34 @@
<category>Time</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="batteryVoltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>Battery Voltage</label>
<description>Battery voltage reported by the device</description>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="batteryTemperature" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Battery Temperature</label>
<description>Battery temperature reported by the device</description>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="gardenSize">
<item-type>Number:Area</item-type>
<label>Garden Size</label>
<description>Garden size mapped by the device</description>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="gardenMap">
<item-type>Image</item-type>
<label>Garden Map</label>
<description>Garden map mapped by the device</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>