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

View File

@@ -0,0 +1,49 @@
/**
* 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.automower.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link AutomowerBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public class AutomowerBindingConstants {
private static final String BINDING_ID = "automower";
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
// generic thing types
public static final ThingTypeUID THING_TYPE_AUTOMOWER = new ThingTypeUID(BINDING_ID, "automower");
// List of all Channel ids
public static final String CHANNEL_MOWER_NAME = "name";
public static final String CHANNEL_STATUS_MODE = "mode";
public static final String CHANNEL_STATUS_ACTIVITY = "activity";
public static final String CHANNEL_STATUS_STATE = "state";
public static final String CHANNEL_STATUS_LAST_UPDATE = "last-update";
public static final String CHANNEL_STATUS_BATTERY = "battery";
public static final String CHANNEL_STATUS_ERROR_CODE = "error-code";
public static final String CHANNEL_STATUS_ERROR_TIMESTAMP = "error-timestamp";
// Automower properties
public static final String AUTOMOWER_ID = "mowerId";
public static final String AUTOMOWER_NAME = "mowerName";
public static final String AUTOMOWER_MODEL = "mowerModel";
public static final String AUTOMOWER_SERIAL_NUMBER = "mowerSerialNumber";
}

View File

@@ -0,0 +1,101 @@
/**
* 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.automower.internal;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.automower.internal.bridge.AutomowerBridgeHandler;
import org.openhab.binding.automower.internal.discovery.AutomowerDiscoveryService;
import org.openhab.binding.automower.internal.things.AutomowerHandler;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link AutomowerHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.automower", service = ThingHandlerFactory.class)
public class AutomowerHandlerFactory extends BaseThingHandlerFactory {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.unmodifiableSet(Stream
.of(AutomowerBridgeHandler.SUPPORTED_THING_TYPES.stream(), AutomowerHandler.SUPPORTED_THING_TYPES.stream())
.flatMap(Function.identity()).collect(Collectors.toSet()));
private final OAuthFactory oAuthFactory;
protected final @NonNullByDefault({}) HttpClient httpClient;
private @Nullable ServiceRegistration<?> automowerDiscoveryServiceRegistration;
@Activate
public AutomowerHandlerFactory(@Reference OAuthFactory oAuthFactory,
@Reference HttpClientFactory httpClientFactory) {
this.oAuthFactory = oAuthFactory;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
if (AutomowerBridgeHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) {
AutomowerBridgeHandler handler = new AutomowerBridgeHandler((Bridge) thing, oAuthFactory, httpClient);
registerAutomowerDiscoveryService(handler);
return handler;
}
if (AutomowerHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) {
return new AutomowerHandler(thing);
}
return null;
}
@Override
protected synchronized void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof AutomowerBridgeHandler) {
if (automowerDiscoveryServiceRegistration != null) {
// remove discovery service, if bridge handler is removed
automowerDiscoveryServiceRegistration.unregister();
}
}
}
private void registerAutomowerDiscoveryService(AutomowerBridgeHandler handler) {
AutomowerDiscoveryService discoveryService = new AutomowerDiscoveryService(handler);
this.automowerDiscoveryServiceRegistration = bundleContext.registerService(DiscoveryService.class.getName(),
discoveryService, new Hashtable<>());
}
}

View File

@@ -0,0 +1,159 @@
/**
* 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.automower.internal.actions;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.automower.internal.things.AutomowerCommand;
import org.openhab.binding.automower.internal.things.AutomowerHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Markus Pfleger - Initial contribution
*/
@ThingActionsScope(name = "automower")
@NonNullByDefault
public class AutomowerActions implements ThingActions, IAutomowerActions {
private final Logger logger = LoggerFactory.getLogger(AutomowerActions.class);
private @Nullable AutomowerHandler handler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
this.handler = (AutomowerHandler) handler;
}
@Override
public @Nullable AutomowerHandler getThingHandler() {
return handler;
}
@Override
@RuleAction(label = "@text/action-start-label", description = "@text/action-start-desc")
public void start(
@ActionInput(name = "duration", label = "@text/action-input-duration-label", description = "@text/action-input-duration-desc") int durationMin) {
AutomowerHandler automowerHandler = handler;
if (automowerHandler == null) {
logger.warn("Automower Action service ThingHandler is null!");
} else {
automowerHandler.sendAutomowerCommand(AutomowerCommand.START, durationMin);
}
}
public static void start(@Nullable ThingActions actions, int durationMin) {
invokeMethodOf(actions).start(durationMin);
}
@Override
@RuleAction(label = "@text/action-pause-label", description = "@text/action-pause-desc")
public void pause() {
AutomowerHandler automowerHandler = handler;
if (automowerHandler == null) {
logger.warn("Automower Action service ThingHandler is null!");
} else {
automowerHandler.sendAutomowerCommand(AutomowerCommand.PAUSE);
}
}
public static void pause(@Nullable ThingActions actions) {
invokeMethodOf(actions).pause();
}
@Override
@RuleAction(label = "@text/action-parkuntilnextschedule-label", description = "@text/action-parkuntilnextschedule-desc")
public void parkUntilNextSchedule() {
AutomowerHandler automowerHandler = handler;
if (automowerHandler == null) {
logger.warn("Automower Action service ThingHandler is null!");
} else {
automowerHandler.sendAutomowerCommand(AutomowerCommand.PARK_UNTIL_NEXT_SCHEDULE);
}
}
public static void parkUntilNextSchedule(@Nullable ThingActions actions) {
invokeMethodOf(actions).parkUntilNextSchedule();
}
@Override
@RuleAction(label = "@text/action-parkuntilfurthernotice-label", description = "@text/action-parkuntilfurthernotice-desc")
public void parkUntilFurtherNotice() {
AutomowerHandler automowerHandler = handler;
if (automowerHandler == null) {
logger.warn("Automower Action service ThingHandler is null!");
} else {
automowerHandler.sendAutomowerCommand(AutomowerCommand.PARK_UNTIL_FURTHER_NOTICE);
}
}
public static void parkUntilFurtherNotice(@Nullable ThingActions actions) {
invokeMethodOf(actions).parkUntilFurtherNotice();
}
@Override
@RuleAction(label = "@text/action-park-label", description = "@text/action-park-desc")
public void park(
@ActionInput(name = "duration", label = "@text/action-input-duration-label", description = "@text/action-input-duration-desc") int durationMin) {
AutomowerHandler automowerHandler = handler;
if (automowerHandler == null) {
logger.warn("Automower Action service ThingHandler is null!");
} else {
automowerHandler.sendAutomowerCommand(AutomowerCommand.PARK, durationMin);
}
}
public static void park(@Nullable ThingActions actions, int durationMin) {
invokeMethodOf(actions).park(durationMin);
}
@Override
@RuleAction(label = "@text/action-resumeschedule-label", description = "@text/action-resumeschedule-desc")
public void resumeSchedule() {
AutomowerHandler automowerHandler = handler;
if (automowerHandler == null) {
logger.warn("Automower Action service ThingHandler is null!");
} else {
automowerHandler.sendAutomowerCommand(AutomowerCommand.RESUME_SCHEDULE);
}
}
public static void resumeSchedule(@Nullable ThingActions actions) {
invokeMethodOf(actions).resumeSchedule();
}
private static IAutomowerActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(AutomowerActions.class.getName())) {
if (actions instanceof AutomowerActions) {
return (IAutomowerActions) actions;
} else {
return (IAutomowerActions) Proxy.newProxyInstance(IAutomowerActions.class.getClassLoader(),
new Class[] { IAutomowerActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of IAutomowerActions");
}
}

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.automower.internal.actions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Actions that can be executed for an automower
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public interface IAutomowerActions {
void resumeSchedule();
void park(int durationMin);
void parkUntilFurtherNotice();
void parkUntilNextSchedule();
void pause();
void start(int durationMin);
}

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.automower.internal.bridge;
import java.io.IOException;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.AutomowerConnectApi;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.Mower;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.MowerCommand;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.MowerCommandAttributes;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.MowerCommandRequest;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.MowerListResult;
import org.openhab.binding.automower.internal.rest.exceptions.AutomowerCommunicationException;
import org.openhab.binding.automower.internal.things.AutomowerCommand;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
/**
* The {@link AutomowerBridge} allows the communication to the various Husqvarna rest apis like the
* AutomowerConnectApi or the AuthenticationApi
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public class AutomowerBridge {
private final OAuthClientService authService;
private final String appKey;
private final String userName;
private final String password;
private final AutomowerConnectApi automowerApi;
public AutomowerBridge(OAuthClientService authService, String appKey, String userName, String password,
HttpClient httpClient, ScheduledExecutorService scheduler) {
this.authService = authService;
this.appKey = appKey;
this.userName = userName;
this.password = password;
this.automowerApi = new AutomowerConnectApi(httpClient);
}
private AccessTokenResponse authenticate() throws AutomowerCommunicationException {
try {
AccessTokenResponse result = authService.getAccessTokenResponse();
if (result == null) {
result = authService.getAccessTokenByResourceOwnerPasswordCredentials(userName, password, null);
}
return result;
} catch (OAuthException | IOException | OAuthResponseException e) {
throw new AutomowerCommunicationException("Unable to authenticate", e);
}
}
/**
* @return A result containing a list of mowers that are available for the current user
* @throws AutomowerCommunicationException In case the query cannot be executed successfully
*/
public MowerListResult getAutomowers() throws AutomowerCommunicationException {
return automowerApi.getMowers(appKey, authenticate().getAccessToken());
}
/**
* @param id The id of the mower to query
* @return A detailed status of the mower with the specified id
* @throws AutomowerCommunicationException In case the query cannot be executed successfully
*/
public Mower getAutomowerStatus(String id) throws AutomowerCommunicationException {
return automowerApi.getMower(appKey, authenticate().getAccessToken(), id).getData();
}
/**
* Sends a command to the automower with the specified id
*
* @param id The id of the mower
* @param command The command that should be sent. Valid values are: "Start", "ResumeSchedule", "Pause", "Park",
* "ParkUntilNextSchedule", "ParkUntilFurtherNotice"
* @param commandDuration The duration of the command. This is only evaluated for "Start" and "Park" commands
* @throws AutomowerCommunicationException In case the query cannot be executed successfully
*/
public void sendAutomowerCommand(String id, AutomowerCommand command, long commandDuration)
throws AutomowerCommunicationException {
MowerCommandAttributes attributes = new MowerCommandAttributes();
attributes.setDuration(commandDuration);
MowerCommand mowerCommand = new MowerCommand();
mowerCommand.setType(command.getCommand());
mowerCommand.setAttributes(attributes);
MowerCommandRequest request = new MowerCommandRequest();
request.setData(mowerCommand);
automowerApi.sendCommand(appKey, authenticate().getAccessToken(), id, request);
}
}

View File

@@ -0,0 +1,65 @@
/**
* 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.automower.internal.bridge;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link AutomowerBridgeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public final class AutomowerBridgeConfiguration {
private @Nullable String appKey;
private @Nullable String userName;
private @Nullable String password;
private @Nullable Integer pollingInterval;
/**
* @return The polling interval for the automower state in s
*/
public @Nullable Integer getPollingInterval() {
return pollingInterval;
}
public void setPollingInterval(Integer pollingInterval) {
this.pollingInterval = pollingInterval;
}
public @Nullable String getAppKey() {
return appKey;
}
public void setAppKey(String appKey) {
this.appKey = appKey;
}
public @Nullable String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public @Nullable String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -0,0 +1,161 @@
/**
* 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.automower.internal.bridge;
import static org.openhab.binding.automower.internal.AutomowerBindingConstants.THING_TYPE_BRIDGE;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.MowerListResult;
import org.openhab.binding.automower.internal.rest.exceptions.AutomowerCommunicationException;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AutomowerBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public class AutomowerBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(AutomowerBridgeHandler.class);
private static final String HUSQVARNA_API_TOKEN_URL = "https://api.authentication.husqvarnagroup.dev/v1/oauth2/token";
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
private static final long DEFAULT_POLLING_INTERVAL_S = TimeUnit.HOURS.toSeconds(1);
private final OAuthFactory oAuthFactory;
private @NonNullByDefault({}) OAuthClientService oAuthService;
private @Nullable ScheduledFuture<?> automowerBridgePollingJob;
private @Nullable AutomowerBridge bridge;
private final HttpClient httpClient;
public AutomowerBridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) {
super(bridge);
this.oAuthFactory = oAuthFactory;
this.httpClient = httpClient;
}
private void pollAutomowers(AutomowerBridge bridge) {
MowerListResult automowers;
try {
automowers = bridge.getAutomowers();
updateStatus(ThingStatus.ONLINE);
logger.debug("Found {} automowers", automowers.getData().size());
} catch (AutomowerCommunicationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/comm-error-query-mowers-failed");
logger.warn("Unable to fetch automowers: {}", e.getMessage());
}
}
@Override
public void dispose() {
AutomowerBridge currentBridge = bridge;
if (currentBridge != null) {
stopAutomowerBridgePolling(currentBridge);
bridge = null;
}
oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
}
@Override
public void initialize() {
AutomowerBridgeConfiguration bridgeConfiguration = getConfigAs(AutomowerBridgeConfiguration.class);
final String appKey = bridgeConfiguration.getAppKey();
final String userName = bridgeConfiguration.getUserName();
final String password = bridgeConfiguration.getPassword();
final Integer pollingIntervalS = bridgeConfiguration.getPollingInterval();
if (appKey == null || appKey.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-app-key");
} else if (userName == null || userName.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-username");
} else if (password == null || password.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-password");
} else if (pollingIntervalS != null && pollingIntervalS < 1) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/conf-error-invalid-polling-interval");
} else {
oAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(), HUSQVARNA_API_TOKEN_URL,
null, appKey, null, null, null);
if (bridge == null) {
AutomowerBridge currentBridge = new AutomowerBridge(oAuthService, appKey, userName, password,
httpClient, scheduler);
bridge = currentBridge;
startAutomowerBridgePolling(currentBridge, pollingIntervalS);
}
updateStatus(ThingStatus.UNKNOWN);
}
}
private void startAutomowerBridgePolling(AutomowerBridge bridge, @Nullable Integer pollingIntervalS) {
ScheduledFuture<?> currentPollingJob = automowerBridgePollingJob;
if (currentPollingJob == null) {
final long pollingIntervalToUse = pollingIntervalS == null ? DEFAULT_POLLING_INTERVAL_S : pollingIntervalS;
automowerBridgePollingJob = scheduler.scheduleWithFixedDelay(() -> pollAutomowers(bridge), 1,
pollingIntervalToUse, TimeUnit.SECONDS);
}
}
private void stopAutomowerBridgePolling(AutomowerBridge bridge) {
ScheduledFuture<?> currentPollingJob = automowerBridgePollingJob;
if (currentPollingJob != null) {
currentPollingJob.cancel(true);
automowerBridgePollingJob = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
public @Nullable AutomowerBridge getAutomowerBridge() {
return bridge;
}
public Optional<MowerListResult> getAutomowers() {
AutomowerBridge currentBridge = bridge;
if (currentBridge == null) {
return Optional.empty();
}
try {
return Optional.of(currentBridge.getAutomowers());
} catch (AutomowerCommunicationException e) {
logger.debug("Bridge cannot get list of available automowers {}", e.getMessage());
return Optional.empty();
}
}
}

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.automower.internal.discovery;
import static org.openhab.binding.automower.internal.AutomowerBindingConstants.THING_TYPE_AUTOMOWER;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.automower.internal.AutomowerBindingConstants;
import org.openhab.binding.automower.internal.bridge.AutomowerBridgeHandler;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.Mower;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.MowerListResult;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
/**
* The {@link AutomowerDiscoveryService} is responsible for discovering new mowers available for the
* configured app key.
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public class AutomowerDiscoveryService extends AbstractDiscoveryService {
private final AutomowerBridgeHandler bridgeHandler;
public AutomowerDiscoveryService(AutomowerBridgeHandler bridgeHandler) {
super(Collections.singleton(THING_TYPE_AUTOMOWER), 10, false);
this.bridgeHandler = bridgeHandler;
}
@Override
protected void startScan() {
Optional<MowerListResult> registeredMowers = bridgeHandler.getAutomowers();
registeredMowers.ifPresent(mowers -> {
for (Mower mower : mowers.getData()) {
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
ThingTypeUID thingTypeUID = THING_TYPE_AUTOMOWER;
ThingUID mowerThingUid = new ThingUID(THING_TYPE_AUTOMOWER, bridgeUID, mower.getId());
Map<String, Object> properties = new HashMap<>();
properties.put(AutomowerBindingConstants.AUTOMOWER_ID, mower.getId());
properties.put(AutomowerBindingConstants.AUTOMOWER_SERIAL_NUMBER,
mower.getAttributes().getSystem().getSerialNumber());
properties.put(AutomowerBindingConstants.AUTOMOWER_MODEL, mower.getAttributes().getSystem().getModel());
properties.put(AutomowerBindingConstants.AUTOMOWER_NAME, mower.getAttributes().getSystem().getName());
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(mowerThingUid)
.withThingType(thingTypeUID).withProperties(properties).withBridge(bridgeUID)
.withRepresentationProperty(AutomowerBindingConstants.AUTOMOWER_ID)
.withLabel(mower.getAttributes().getSystem().getName() + " (Automower "
+ mower.getAttributes().getSystem().getModel() + ")")
.build();
thingDiscovered(discoveryResult);
}
});
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.automower.internal.rest.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Base class for the Husqvarna apis
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public abstract class HusqvarnaApi {
private final HttpClient httpClient;
protected final Gson gson;
protected HusqvarnaApi(HttpClient httpClient) {
this.httpClient = httpClient;
GsonBuilder gsonBuilder = new GsonBuilder();
gson = gsonBuilder.create();
}
protected abstract String getBaseUrl();
protected HttpClient getHttpClient() {
return httpClient;
}
}

View File

@@ -0,0 +1,131 @@
/**
* 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.automower.internal.rest.api.automowerconnect;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.automower.internal.rest.api.HusqvarnaApi;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.MowerCommandRequest;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.MowerListResult;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.MowerResult;
import org.openhab.binding.automower.internal.rest.exceptions.AutomowerCommunicationException;
import org.openhab.binding.automower.internal.rest.exceptions.UnauthorizedException;
import com.google.gson.JsonSyntaxException;
/**
* Allows access to the AutomowerConnectApi
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public class AutomowerConnectApi extends HusqvarnaApi {
public AutomowerConnectApi(HttpClient httpClient) {
super(httpClient);
}
@Override
public String getBaseUrl() {
return "https://api.amc.husqvarna.dev/v1";
}
public MowerListResult getMowers(String appKey, String token) throws AutomowerCommunicationException {
final Request request = getHttpClient().newRequest(getBaseUrl() + "/mowers");
request.method(HttpMethod.GET);
ContentResponse response = executeRequest(appKey, token, request);
return parseResponse(response, MowerListResult.class);
}
public MowerResult getMower(String appKey, String token, String mowerId) throws AutomowerCommunicationException {
final Request request = getHttpClient().newRequest(getBaseUrl() + "/mowers/" + mowerId);
request.method(HttpMethod.GET);
ContentResponse response = executeRequest(appKey, token, request);
return parseResponse(response, MowerResult.class);
}
public void sendCommand(String appKey, String token, String id, MowerCommandRequest command)
throws AutomowerCommunicationException {
final Request request = getHttpClient().newRequest(getBaseUrl() + "/mowers/" + id + "/actions");
request.method(HttpMethod.POST);
request.content(new StringContentProvider(gson.toJson(command)));
ContentResponse response = executeRequest(appKey, token, request);
checkForError(response, response.getStatus());
}
private ContentResponse executeRequest(String appKey, String token, final Request request)
throws AutomowerCommunicationException {
request.timeout(10, TimeUnit.SECONDS);
request.header("Authorization-Provider", "husqvarna");
request.header("Authorization", "Bearer " + token);
request.header("X-Api-Key", appKey);
request.header("Content-Type", "application/vnd.api+json");
ContentResponse response;
try {
response = request.send();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new AutomowerCommunicationException(e);
}
return response;
}
private <T> T parseResponse(ContentResponse response, Class<T> type) throws AutomowerCommunicationException {
int statusCode = response.getStatus();
checkForError(response, statusCode);
try {
return gson.fromJson(response.getContentAsString(), type);
} catch (JsonSyntaxException e) {
throw new AutomowerCommunicationException(e);
}
}
private void checkForError(ContentResponse response, int statusCode) throws AutomowerCommunicationException {
if (statusCode >= 200 && statusCode < 300) {
return;
}
switch (statusCode) {
case HttpStatus.NOT_FOUND_404:
throw new AutomowerCommunicationException(statusCode, "Target '" + response.getRequest().getURI()
+ "' seems to be not available: " + response.getContentAsString());
case HttpStatus.FORBIDDEN_403:
case HttpStatus.UNAUTHORIZED_401:
throw new UnauthorizedException(statusCode, response.getContentAsString());
default:
throw new AutomowerCommunicationException(statusCode, response.getContentAsString());
}
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public enum Activity {
UNKNOWN,
NOT_APPLICABLE,
MOWING,
GOING_HOME,
CHARGING,
LEAVING,
PARKED_IN_CS,
STOPPED_IN_GARDEN
}

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.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class Battery {
private int batteryPercent;
public int getBatteryPercent() {
return batteryPercent;
}
public void setBatteryPercent(int batteryPercent) {
this.batteryPercent = batteryPercent;
}
}

View File

@@ -0,0 +1,20 @@
/**
* 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.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class Calendar {
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class Metadata {
private boolean connected;
private long statusTimestamp;
public boolean isConnected() {
return connected;
}
public void setConnected(boolean connected) {
this.connected = connected;
}
public long getStatusTimestamp() {
return statusTimestamp;
}
public void setStatusTimestamp(long statusTimestamp) {
this.statusTimestamp = statusTimestamp;
}
}

View File

@@ -0,0 +1,24 @@
/**
* 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.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public enum Mode {
MAIN_AREA,
SECONDARY_AREA,
HOME,
DEMO,
UNKNOWN
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class Mower {
private String type;
private String id;
private MowerData attributes;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public MowerData getAttributes() {
return attributes;
}
public void setAttributes(MowerData attributes) {
this.attributes = attributes;
}
}

View File

@@ -0,0 +1,65 @@
/**
* 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.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class MowerApp {
private Mode mode;
private Activity activity;
private State state;
private int errorCode;
private long errorCodeTimestamp;
public Mode getMode() {
return mode;
}
public void setMode(Mode mode) {
this.mode = mode;
}
public Activity getActivity() {
return activity;
}
public void setActivity(Activity activity) {
this.activity = activity;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
public int getErrorCode() {
return errorCode;
}
public void setErrorCode(int errorCode) {
this.errorCode = errorCode;
}
public long getErrorCodeTimestamp() {
return errorCodeTimestamp;
}
public void setErrorCodeTimestamp(long errorCodeTimestamp) {
this.errorCodeTimestamp = errorCodeTimestamp;
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class MowerCommand {
private String type;
private MowerCommandAttributes attributes;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public MowerCommandAttributes getAttributes() {
return attributes;
}
public void setAttributes(MowerCommandAttributes attributes) {
this.attributes = attributes;
}
}

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.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class MowerCommandAttributes {
private long duration;
public long getDuration() {
return duration;
}
public void setDuration(long duration) {
this.duration = duration;
}
}

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.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class MowerCommandRequest {
private MowerCommand data;
public MowerCommand getData() {
return data;
}
public void setData(MowerCommand data) {
this.data = data;
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class MowerData {
private System system;
private Battery battery;
private MowerApp mower;
private Calendar calendar;
private Planner planner;
private Metadata metadata;
public System getSystem() {
return system;
}
public void setSystem(System system) {
this.system = system;
}
public Battery getBattery() {
return battery;
}
public void setBattery(Battery battery) {
this.battery = battery;
}
public MowerApp getMower() {
return mower;
}
public void setMower(MowerApp mower) {
this.mower = mower;
}
public Calendar getCalendar() {
return calendar;
}
public void setCalendar(Calendar calendar) {
this.calendar = calendar;
}
public Planner getPlanner() {
return planner;
}
public void setPlanner(Planner planner) {
this.planner = planner;
}
public Metadata getMetadata() {
return metadata;
}
public void setMetadata(Metadata metadata) {
this.metadata = metadata;
}
}

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.automower.internal.rest.api.automowerconnect.dto;
import java.util.List;
/**
* @author Markus Pfleger - Initial contribution
*/
public class MowerListResult {
private List<Mower> data;
public List<Mower> getData() {
return data;
}
public void setData(List<Mower> data) {
this.data = data;
}
}

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.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class MowerResult {
private Mower data;
public Mower getData() {
return data;
}
public void setData(Mower data) {
this.data = data;
}
}

View File

@@ -0,0 +1,20 @@
/**
* 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.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class Planner {
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public enum State {
UNKNOWN,
NOT_APPLICABLE,
PAUSED,
IN_OPERATION,
WAIT_UPDATING,
WAIT_POWER_UP,
RESTRICTED,
OFF,
STOPPED,
ERROR,
FATAL_ERROR,
ERROR_AT_POWER_UP
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.automower.internal.rest.api.automowerconnect.dto;
/**
* @author Markus Pfleger - Initial contribution
*/
public class System {
private String name;
private String model;
private String serialNumber;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getSerialNumber() {
return serialNumber;
}
public void setSerialNumber(String serialNumber) {
this.serialNumber = serialNumber;
}
}

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.automower.internal.rest.exceptions;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* An exception that occurred while communicating with an automower or an automower bridge
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public class AutomowerCommunicationException extends IOException {
private static final long serialVersionUID = 1L;
private int statusCode = -1;
public AutomowerCommunicationException(Exception e) {
super(e);
}
public AutomowerCommunicationException(int statusCode, Exception e) {
super(e);
this.statusCode = statusCode;
}
public AutomowerCommunicationException(int statusCode) {
this.statusCode = statusCode;
}
public AutomowerCommunicationException(int statusCode, String message) {
super(message);
this.statusCode = statusCode;
}
public AutomowerCommunicationException(String message, Exception e) {
super(message, e);
}
public AutomowerCommunicationException(String message) {
super(message);
}
public int getStatusCode() {
return statusCode;
}
@Override
public String getMessage() {
return "Rest call failed: statusCode=" + statusCode + ", message=" + super.getMessage();
}
@Override
public String toString() {
return getClass().getSimpleName() + ": statusCode=" + statusCode + ", message=" + super.getMessage()
+ ", cause: " + getCause();
}
}

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.automower.internal.rest.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public class UnauthorizedException extends AutomowerCommunicationException {
private static final long serialVersionUID = 1L;
public UnauthorizedException(int statusCode, Exception e) {
super(statusCode, e);
}
public UnauthorizedException(int statusCode) {
super(statusCode);
}
public UnauthorizedException(int statusCode, String message) {
super(statusCode, message);
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.automower.internal.rest.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public class UnavailableException extends AutomowerCommunicationException {
private static final long serialVersionUID = 1L;
public UnavailableException(int statusCode, Exception e) {
super(statusCode, e);
}
}

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.automower.internal.things;
/**
* @author Markus Pfleger - Initial contribution
*/
public enum AutomowerCommand {
START("Start"),
RESUME_SCHEDULE("ResumeSchedule"),
PAUSE("Pause"),
PARK("Park"),
PARK_UNTIL_NEXT_SCHEDULE("ParkUntilNextSchedule"),
PARK_UNTIL_FURTHER_NOTICE("ParkUntilFurtherNotice");
private final String command;
private AutomowerCommand(String command) {
this.command = command;
}
public String getCommand() {
return command;
}
}

View File

@@ -0,0 +1,45 @@
/**
* 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.automower.internal.things;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link AutomowerConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public class AutomowerConfiguration {
public @Nullable String mowerId;
public @Nullable Integer pollingInterval;
@Nullable
public String getMowerId() {
return mowerId;
}
public void setMowerId(String mowerId) {
this.mowerId = mowerId;
}
public @Nullable Integer getPollingInterval() {
return pollingInterval;
}
public void setPollingInterval(Integer pollingInterval) {
this.pollingInterval = pollingInterval;
}
}

View File

@@ -0,0 +1,283 @@
/**
* 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.automower.internal.things;
import static org.openhab.binding.automower.internal.AutomowerBindingConstants.*;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.measure.quantity.Dimensionless;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.automower.internal.AutomowerBindingConstants;
import org.openhab.binding.automower.internal.actions.AutomowerActions;
import org.openhab.binding.automower.internal.bridge.AutomowerBridge;
import org.openhab.binding.automower.internal.bridge.AutomowerBridgeHandler;
import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.Mower;
import org.openhab.binding.automower.internal.rest.exceptions.AutomowerCommunicationException;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AutomowerHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Markus Pfleger - Initial contribution
*/
@NonNullByDefault
public class AutomowerHandler extends BaseThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_AUTOMOWER);
private static final String NO_ID = "NO_ID";
private static final long DEFAULT_COMMAND_DURATION_MIN = 60;
private static final long DEFAULT_POLLING_INTERVAL_S = TimeUnit.MINUTES.toSeconds(10);
private final Logger logger = LoggerFactory.getLogger(AutomowerHandler.class);
private AtomicReference<String> automowerId = new AtomicReference<String>(NO_ID);
private long lastQueryTimeMs = 0L;
private @Nullable ScheduledFuture<?> automowerPollingJob;
private long maxQueryFrequencyNanos = TimeUnit.MINUTES.toNanos(1);
private Runnable automowerPollingRunnable = () -> {
Bridge bridge = getBridge();
if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
updateAutomowerState();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
};
public AutomowerHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
refreshChannels(channelUID);
}
}
private void refreshChannels(ChannelUID channelUID) {
updateAutomowerState();
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(AutomowerActions.class);
}
@Override
public void initialize() {
Bridge bridge = getBridge();
if (bridge != null) {
AutomowerConfiguration currentConfig = getConfigAs(AutomowerConfiguration.class);
final String configMowerId = currentConfig.getMowerId();
final Integer pollingIntervalS = currentConfig.getPollingInterval();
if (configMowerId == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/conf-error-no-mower-id");
} else if (pollingIntervalS != null && pollingIntervalS < 1) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/conf-error-invalid-polling-interval");
} else {
automowerId.set(configMowerId);
startAutomowerPolling(pollingIntervalS);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
}
}
@Nullable
private AutomowerBridge getAutomowerBridge() {
Bridge bridge = getBridge();
if (bridge != null) {
ThingHandler handler = bridge.getHandler();
if (handler instanceof AutomowerBridgeHandler) {
AutomowerBridgeHandler bridgeHandler = (AutomowerBridgeHandler) handler;
return bridgeHandler.getAutomowerBridge();
}
}
return null;
}
@Override
public void dispose() {
if (!automowerId.get().equals(NO_ID)) {
stopAutomowerPolling();
automowerId.set(NO_ID);
}
}
private void startAutomowerPolling(@Nullable Integer pollingIntervalS) {
if (automowerPollingJob == null) {
final long pollingIntervalToUse = pollingIntervalS == null ? DEFAULT_POLLING_INTERVAL_S : pollingIntervalS;
automowerPollingJob = scheduler.scheduleWithFixedDelay(automowerPollingRunnable, 1, pollingIntervalToUse,
TimeUnit.SECONDS);
}
}
private void stopAutomowerPolling() {
if (automowerPollingJob != null) {
automowerPollingJob.cancel(true);
automowerPollingJob = null;
}
}
private boolean isValidResult(Mower mower) {
return mower.getAttributes() != null && mower.getAttributes().getMetadata() != null
&& mower.getAttributes().getBattery() != null && mower.getAttributes().getSystem() != null;
}
private boolean isConnected(Mower mower) {
return mower.getAttributes() != null && mower.getAttributes().getMetadata() != null
&& mower.getAttributes().getMetadata().isConnected();
}
private synchronized void updateAutomowerState() {
if (System.nanoTime() - lastQueryTimeMs > maxQueryFrequencyNanos) {
lastQueryTimeMs = System.nanoTime();
String id = automowerId.get();
try {
AutomowerBridge automowerBridge = getAutomowerBridge();
if (automowerBridge != null) {
Mower mower = automowerBridge.getAutomowerStatus(id);
if (isValidResult(mower)) {
initializeProperties(mower);
updateChannelState(mower);
if (isConnected(mower)) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/comm-error-mower-not-connected-to-cloud");
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/comm-error-query-mower-failed");
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/conf-error-no-bridge");
}
} catch (AutomowerCommunicationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/comm-error-query-mower-failed");
logger.warn("Unable to query automower status for: {}. Error: {}", id, e.getMessage());
}
}
}
/**
* Sends a command to the automower with the default duration of 60min
*
* @param command The command that should be sent. Valid values are: "Start", "ResumeSchedule", "Pause", "Park",
* "ParkUntilNextSchedule", "ParkUntilFurtherNotice"
*/
public void sendAutomowerCommand(AutomowerCommand command) {
sendAutomowerCommand(command, DEFAULT_COMMAND_DURATION_MIN);
}
/**
* Sends a command to the automower with the given duration
*
* @param command The command that should be sent. Valid values are: "Start", "ResumeSchedule", "Pause", "Park",
* "ParkUntilNextSchedule", "ParkUntilFurtherNotice"
* @param commandDurationMinutes The duration of the command in minutes. This is only evaluated for "Start" and
* "Park" commands
*/
public void sendAutomowerCommand(AutomowerCommand command, long commandDurationMinutes) {
String id = automowerId.get();
try {
AutomowerBridge automowerBridge = getAutomowerBridge();
if (automowerBridge != null) {
automowerBridge.sendAutomowerCommand(id, command, commandDurationMinutes);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-bridge");
}
} catch (AutomowerCommunicationException e) {
logger.warn("Unable to send command to automower: {}, Error: {}", id, e.getMessage());
}
updateAutomowerState();
}
private void updateChannelState(Mower mower) {
if (isValidResult(mower)) {
updateState(CHANNEL_MOWER_NAME, new StringType(mower.getAttributes().getSystem().getName()));
updateState(CHANNEL_STATUS_MODE, new StringType(mower.getAttributes().getMower().getMode().name()));
updateState(CHANNEL_STATUS_ACTIVITY, new StringType(mower.getAttributes().getMower().getActivity().name()));
updateState(CHANNEL_STATUS_STATE, new StringType(mower.getAttributes().getMower().getState().name()));
Instant statusTimestamp = Instant.ofEpochMilli(mower.getAttributes().getMetadata().getStatusTimestamp());
updateState(CHANNEL_STATUS_LAST_UPDATE,
new DateTimeType(ZonedDateTime.ofInstant(statusTimestamp, ZoneId.systemDefault())));
updateState(CHANNEL_STATUS_BATTERY, new QuantityType<Dimensionless>(
mower.getAttributes().getBattery().getBatteryPercent(), SmartHomeUnits.PERCENT));
updateState(CHANNEL_STATUS_ERROR_CODE, new DecimalType(mower.getAttributes().getMower().getErrorCode()));
Instant errorCodeTimestamp = Instant.ofEpochMilli(mower.getAttributes().getMower().getErrorCodeTimestamp());
updateState(CHANNEL_STATUS_ERROR_TIMESTAMP,
new DateTimeType(ZonedDateTime.ofInstant(errorCodeTimestamp, ZoneId.systemDefault())));
}
}
private void initializeProperties(Mower mower) {
Map<String, String> properties = editProperties();
properties.put(AutomowerBindingConstants.AUTOMOWER_ID, mower.getId());
if (mower.getAttributes() != null && mower.getAttributes().getSystem() != null) {
properties.put(AutomowerBindingConstants.AUTOMOWER_SERIAL_NUMBER,
mower.getAttributes().getSystem().getSerialNumber());
properties.put(AutomowerBindingConstants.AUTOMOWER_MODEL, mower.getAttributes().getSystem().getModel());
properties.put(AutomowerBindingConstants.AUTOMOWER_NAME, mower.getAttributes().getSystem().getName());
}
updateProperties(properties);
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="automower" 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>Automower Binding</name>
<description>Binding to interact with Husquvarna Automower robots.</description>
<author>Markus Pfleger</author>
</binding:binding>

View File

@@ -0,0 +1,62 @@
# binding
binding.automower.name = Automower
binding.automower.description = This binding allows to interact with Husqvarna Automower robots
# automower parameters
thing-type.config.automower.automower.mowerId.label = ID
thing-type.config.automower.automower.mowerName.label = Name
thing-type.config.automower.automower.mowerModel.label = Model
thing-type.config.automower.automower.mowerId.mowerSerialNumber = Serial Number
thing-type.config.automower.automower.connected.label = Connected
thing-type.config.automower.automower.timestamp.label = Last State Update
thing-type.config.automower.automower.batteryPct.label = Battery Percentage
# channel types
channel-type.automower.name.label = Name
channel-type.automower.name.description = Automower name
channel-type.automower.mode.label = Mode
channel-type.automower.mode.description = Mode
channel-type.automower.activity.label = Activity
channel-type.automower.activity.description = Activity
channel-type.automower.state.label = State
channel-type.automower.state.description = State
channel-type.automower.last-update.label = Last Update
channel-type.automower.last-update.description = Last Update
conf-error-no-app-key = Cannot connect to Automower bridge as no app key is available in the configuration
conf-error-no-username = Cannot connect to Automower bridge as no username is available in the configuration
conf-error-no-password = Cannot connect to Automower bridge as no password is available in the configuration
conf-error-invalid-polling-interval = Invalid polling interval specified. The polling interval has to be >= 1
conf-error-no-mower-id = No Automower ID specified. Unable to communicate with the mower without an ID
conf-error-no-bridge = No valid bridge for the automower is available
comm-error-httpclient-init-failed = Unable to initialize http client
comm-error-query-mowers-failed = Unable to query registered mowers
comm-error-query-mower-failed = Unable to query the automower status
comm-error-send-mower-command-failed = Unable to send automower command
comm-error-mower-not-connected-to-cloud = Automower not connected to the cloud
action-start-label = Start
action-start-desc = Starts the automower for a defined amount of time, overriding its schedule
action-pause-label = Pause
action-pause-desc = Pauses the automower wherever it currently is
action-parkuntilnextschedule-label = Park until next schedule
action-parkuntilnextschedule-desc = Parks the automower until the next schedule
action-parkuntilfurthernotice-label = Park until further notice
action-parkuntilfurthernotice-desc = Parks the automower until the schedule is resumed
action-park-label = Park
action-park-desc = Parks the automower for a defined amount of time, overriding its schedule
action-resumeschedule-label = Resume schedule
action-resumeschedule-desc = Resumes the schedule for the automower
action-input-duration-label = Duration
action-input-duration-desc = The duration of the automower command in minutes

View File

@@ -0,0 +1,48 @@
binding.automower.name = Automower Binding
binding.automower.description = Das Automower Binding erlaubt die Interaktion mit Husqvarna Automower Mährobotern mit Automower Connect Unterstützung.
thing-type.automower.bridge.label = Automower Connect Bridge
thing-type.automower.bridge.description = Erlaubt die Kommunikation mit der Husqvarna Automower Connect API
thing-type.config.automower.bridge.appKey.label = Application Key
thing-type.config.automower.bridge.appKey.description = Der Application Key wird für die Kommunication mit der Automower Connect API benötigt. Um diesen zu erhalten muss eine Anwendung auf der Husqvarna Website registriert werden. Diese Anwendung muss mit der "Authentication API" und der "Automower Connect API" verknüpft werden.
thing-type.config.automower.bridge.userName.label = Benutzername
thing-type.config.automower.bridge.userName.description = Der Benutzername für den der Application Key ausgestellt wurde.
thing-type.config.automower.bridge.password.label = Passwort
thing-type.config.automower.bridge.password.description = Das Passwort für den angegebenen Benutzer.
thing-type.config.automower.bridge.pollingInterval.label = Polling Intervall
thing-type.config.automower.bridge.pollingInterval.description = Das Intervall in dem die Verbindung mit dem Automower Connect API überprüft werden soll. Der Standardwert ist 3600s (1h)
thing-type.automower.automower.label = Automower
thing-type.automower.automower.description = Ein Husqvarna Automower
thing-type.config.automower.automower.mowerId.label = Automower Id
thing-type.config.automower.automower.mowerId.label = Die interne ID des Automowers die von der Automower Connect API benutzt wird um einen Automower zu identifizieren
thing-type.config.automower.automower.pollingInterval.label = Polling Intervall
thing-type.config.automower.automower.pollingInterval.label = Das Intervall in dem der Status des Automowers abgefragt werden soll. Der Standardwert ist 300s (5min)
action-start-label = Start
action-start-desc = Startet den Automower für die angegebene Zeit, unabhängig vom Zeitplan
action-pause-label = Pause
action-pause-desc = Pausiert den Automower wo immer er gerade ist
action-parkuntilnextschedule-label = Parken (laut Zeitplan)
action-parkuntilnextschedule-desc = Parkt den Automower uns setzt den Zeitplan fort sobald er geladen wurde
action-parkuntilfurthernotice-label = Parken (bis auf weiteres)
action-parkuntilfurthernotice-desc = Parkt den Automower bis auf weiteres
action-park-label = Parken
action-park-desc = Parkt den Automower für die angegebene Zeit, unabhängig vom Zeitplan
action-resumeschedule-label = Start (Zeitplan fortsetzen)
action-resumeschedule-desc = Startet den Automower und setzt den Zeitplan fort
action-input-duration-label = Duration
action-input-duration-desc = The duration of the automower command in minutes

View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="automower"
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">
<!-- Automower Bridge Thing Type -->
<bridge-type id="bridge">
<label>Automower Connect Bridge</label>
<description>The bridge to communicate with the Automower Connect API</description>
<config-description>
<parameter name="appKey" type="text" required="true">
<label>Application Key</label>
<description>The Application Key is required to communication with the Automower Connect Api. It can be obtained by
registering an Application on the Husqvarna Website. This application also needs to be connected to the
"Authentication API" and the "Automower Connect API"</description>
</parameter>
<parameter name="userName" type="text" required="true">
<label>User Name</label>
<description>The user name for which the application key has been issued</description>
</parameter>
<parameter name="password" type="text" required="true">
<context>Password</context>
<description>The password for the given user</description>
</parameter>
<parameter name="pollingInterval" type="integer" required="false" unit="s">
<label>Polling Interval</label>
<default>3600</default>
<description>How often the available automowers should be queried in seconds</description>
</parameter>
</config-description>
</bridge-type>
<!-- Automower Thing Type -->
<thing-type id="automower">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>Automower</label>
<description>An automatic lawn mower</description>
<channels>
<channel id="name" typeId="nameType"/>
<channel id="mode" typeId="modeType"/>
<channel id="activity" typeId="activityType"/>
<channel id="state" typeId="stateType"/>
<channel id="last-update" typeId="lastUpdateType"/>
<channel id="battery" typeId="batteryType"/>
<channel id="error-code" typeId="errorCodeType"/>
<channel id="error-timestamp" typeId="errorTimestampType"/>
</channels>
<properties>
<property name="mowerId">N/A</property>
<property name="mowerName">N/A</property>
<property name="mowerModel">N/A</property>
<property name="mowerSerialNumber">N/A</property>
</properties>
<config-description>
<parameter name="mowerId" type="text" required="true">
<label>Automower Id</label>
<description>The Id of an automower as used by the Automower Connect Api to identify a mower</description>
</parameter>
<parameter name="pollingInterval" type="integer" required="false" unit="s">
<label>Polling Interval</label>
<default>600</default>
<description>How often the current automower state should be polled in seconds</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="nameType">
<item-type>String</item-type>
<label>Automower Name</label>
<description>The channel providing the automower name</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="modeType">
<item-type>String</item-type>
<label>Mode</label>
<description>The current mode</description>
<state readOnly="true">
<options>
<option value="MAIN_AREA">Main Area</option>
<option value="SECONDARY_AREA">Secondary Area</option>
<option value="HOME">Home</option>
<option value="DEMO">Demo</option>
<option value="UNKNOWN">Unknown</option>
</options>
</state>
</channel-type>
<channel-type id="activityType">
<item-type>String</item-type>
<label>Activity</label>
<description>The current activity</description>
<state readOnly="true">
<options>
<option value="UNKNOWN">Unknown</option>
<option value="NOT_APPLICABLE">N/A</option>
<option value="MOWING">Mowing</option>
<option value="GOING_HOME">Returning to charging station</option>
<option value="CHARGING">Charging</option>
<option value="LEAVING">Leaving charging station</option>
<option value="PARKED_IN_CS">Parked in charging station</option>
<option value="STOPPED_IN_GARDEN">Stopped in garden</option>
</options>
</state>
</channel-type>
<channel-type id="stateType">
<item-type>String</item-type>
<label>State</label>
<description>The current state</description>
<state readOnly="true">
<options>
<option value="UNKNOWN">Unknown</option>
<option value="NOT_APPLICABLE">N/A</option>
<option value="PAUSED">Paused by user</option>
<option value="IN_OPERATION">Working</option>
<option value="WAIT_UPDATING">Downloading new firmware</option>
<option value="WAIT_POWER_UP">Booting mower</option>
<option value="RESTRICTED">Waiting</option>
<option value="OFF">Off</option>
<option value="STOPPED">Stopped- Manual intervention required</option>
<option value="ERROR">Error</option>
<option value="FATAL_ERROR">Fatal error</option>
<option value="ERROR_AT_POWER_UP">Boot error</option>
</options>
</state>
</channel-type>
<channel-type id="lastUpdateType">
<item-type>DateTime</item-type>
<label>Last Update</label>
<description>The time when the mower sent the last update</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="batteryType">
<item-type>Number:Dimensionless</item-type>
<label>Battery</label>
<description>The battery level of the mower at the time of last update</description>
<state readOnly="true" pattern="%d %%"/>
</channel-type>
<channel-type id="errorCodeType">
<item-type>Number</item-type>
<label>Error Code</label>
<description>The error code at the time of last update</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="errorTimestampType">
<item-type>DateTime</item-type>
<label>Error Time</label>
<description>The time when the error occurred</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>