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 |
+| 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 |
+| 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
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
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);
}
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;
// 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);
}
}
}
+ /**
+ * 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.
*
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.
*
public @Nullable String username;
public @Nullable String password;
public long refresh = 180;
- public long cuttingTimeRefresh = 60;
+ public long cuttingTimeMapRefresh = 60;
}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
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;
private @NonNullByDefault({}) IndegoController controller;
private @Nullable ScheduledFuture<?> statePollFuture;
- private @Nullable ScheduledFuture<?> cuttingTimePollFuture;
+ private @Nullable ScheduledFuture<?> cuttingTimeMapPollFuture;
private boolean propertiesInitialized;
private int previousStateCode;
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
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());
}
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;
}
}
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");
}
DeviceStateResponse state = controller.getState();
- updateStatus(ThingStatus.ONLINE);
updateState(state);
// When state code changed, refresh cutting times immediately.
}
}
- 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 {
}
}
+ 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());
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;
}
# 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
# 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
# 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
<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">
<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>
<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>