Skip to content

Commit

Permalink
Add support for multiple mowers
Browse files Browse the repository at this point in the history
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
  • Loading branch information
jlaur committed Apr 6, 2023
1 parent 392f1ee commit 4c064d3
Show file tree
Hide file tree
Showing 11 changed files with 593 additions and 317 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class BoschIndegoBindingConstants {
public static final String BINDING_ID = "boschindego";

// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_INDEGO = new ThingTypeUID(BINDING_ID, "indego");

// List of all Channel ids
Expand All @@ -47,7 +48,7 @@ public class BoschIndegoBindingConstants {
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);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_INDEGO);

// Bosch SingleKey ID OAuth2
private static final String BSK_BASE_URI = "https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,23 @@
*/
package org.openhab.binding.boschindego.internal;

import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.THING_TYPE_INDEGO;
import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;

import java.util.Hashtable;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.boschindego.internal.discovery.IndegoDiscoveryService;
import org.openhab.binding.boschindego.internal.handler.BoschAccountHandler;
import org.openhab.binding.boschindego.internal.handler.BoschIndegoHandler;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.i18n.TranslationProvider;
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;
Expand Down Expand Up @@ -70,8 +76,14 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();

if (THING_TYPE_INDEGO.equals(thingTypeUID)) {
return new BoschIndegoHandler(thing, httpClient, oAuthFactory, translationProvider, timeZoneProvider);
if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
var accountHandler = new BoschAccountHandler((Bridge) thing, httpClient, oAuthFactory);
var discoveryService = new IndegoDiscoveryService(accountHandler);
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>());

return accountHandler;
} else if (THING_TYPE_INDEGO.equals(thingTypeUID)) {
return new BoschIndegoHandler(thing, httpClient, translationProvider, timeZoneProvider);
}

return null;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
/**
* Copyright (c) 2010-2023 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;

import java.time.Duration;
import java.time.Instant;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
import org.openhab.binding.boschindego.internal.dto.PredictiveAdjustment;
import org.openhab.binding.boschindego.internal.dto.PredictiveStatus;
import org.openhab.binding.boschindego.internal.dto.request.SetStateRequest;
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.IndegoTimeoutException;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.library.types.RawType;

/**
* Controller for communicating with a Bosch Indego device through Bosch services.
* This class provides methods for retrieving state information as well as controlling
* the device.
*
* The implementation is based on zazaz-de/iot-device-bosch-indego-controller, but
* rewritten from scratch to use Jetty HTTP client for HTTP communication and GSON for
* JSON parsing. Thanks to Oliver Schünemann for providing the original implementation.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class IndegoDeviceController extends IndegoController {

private String serialNumber = "";

/**
* Initialize the controller instance.
*
* @param httpClient the HttpClient for communicating with the service
* @param oAuthClientService the OAuthClientService for authorization
* @param serialNumber the serial number of the device instance
*/
public IndegoDeviceController(HttpClient httpClient, OAuthClientService oAuthClientService, String serialNumber) {
super(httpClient, oAuthClientService);
if (serialNumber.isBlank()) {
throw new IllegalArgumentException("Serial number must be provided");
}
this.serialNumber = serialNumber;
}

/**
* Queries the device state from the server.
*
* @return the device state
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public DeviceStateResponse getState() throws IndegoAuthenticationException, IndegoException {
return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/state", DeviceStateResponse.class);
}

/**
* Queries the device state from the server. This overload will return when the state
* has changed, or the timeout has been reached.
*
* @param timeout Maximum time to wait for response
* @return the device state
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public DeviceStateResponse getState(Duration timeout) throws IndegoAuthenticationException, IndegoException {
return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/state?longpoll=true&timeout=" + timeout.getSeconds(),
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 IndegoTimeoutException if device cannot be reached (gateway timeout error)
* @throws IndegoException if any communication or parsing error occurred
*/
public OperatingDataResponse getOperatingData()
throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/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 getRawRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/map");
}

/**
* Queries the calendar.
*
* @return the calendar
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public DeviceCalendarResponse getCalendar() throws IndegoAuthenticationException, IndegoException {
DeviceCalendarResponse calendar = getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/calendar",
DeviceCalendarResponse.class);
return calendar;
}

/**
* Sends a command to the Indego device.
*
* @param command the control command to send to the device
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoInvalidCommandException if the command was not processed correctly
* @throws IndegoException if any communication or parsing error occurred
*/
public void sendCommand(DeviceCommand command)
throws IndegoAuthenticationException, IndegoInvalidCommandException, IndegoException {
SetStateRequest request = new SetStateRequest();
request.state = command.getActionCode();
putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/state", request);
}

/**
* Queries the predictive weather forecast.
*
* @return the weather forecast DTO
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public LocationWeatherResponse getWeather() throws IndegoAuthenticationException, IndegoException {
return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/weather", LocationWeatherResponse.class);
}

/**
* Queries the predictive adjustment.
*
* @return the predictive adjustment
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public int getPredictiveAdjustment() throws IndegoAuthenticationException, IndegoException {
return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/useradjustment",
PredictiveAdjustment.class).adjustment;
}

/**
* Sets the predictive adjustment.
*
* @param adjust the predictive adjustment
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public void setPredictiveAdjustment(final int adjust) throws IndegoAuthenticationException, IndegoException {
final PredictiveAdjustment adjustment = new PredictiveAdjustment();
adjustment.adjustment = adjust;
putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/useradjustment", adjustment);
}

/**
* Queries predictive moving.
*
* @return predictive moving
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public boolean getPredictiveMoving() throws IndegoAuthenticationException, IndegoException {
return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive", PredictiveStatus.class).enabled;
}

/**
* Sets predictive moving.
*
* @param enable
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public void setPredictiveMoving(final boolean enable) throws IndegoAuthenticationException, IndegoException {
final PredictiveStatus status = new PredictiveStatus();
status.enabled = enable;
putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive", status);
}

/**
* Queries predictive last cutting as {@link Instant}.
*
* @return predictive last cutting
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public @Nullable Instant getPredictiveLastCutting() throws IndegoAuthenticationException, IndegoException {
try {
return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/lastcutting",
PredictiveLastCuttingResponse.class).getLastCutting();
} catch (IndegoInvalidResponseException e) {
if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
return null;
}
throw e;
}
}

/**
* Queries predictive next cutting as {@link Instant}.
*
* @return predictive next cutting
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public @Nullable Instant getPredictiveNextCutting() throws IndegoAuthenticationException, IndegoException {
try {
return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/nextcutting",
PredictiveNextCuttingResponse.class).getNextCutting();
} catch (IndegoInvalidResponseException e) {
if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
return null;
}
throw e;
}
}

/**
* Queries predictive exclusion time.
*
* @return predictive exclusion time DTO
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public DeviceCalendarResponse getPredictiveExclusionTime() throws IndegoAuthenticationException, IndegoException {
return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/calendar", DeviceCalendarResponse.class);
}

/**
* Sets predictive exclusion time.
*
* @param calendar calendar DTO
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public void setPredictiveExclusionTime(final DeviceCalendarResponse calendar)
throws IndegoAuthenticationException, IndegoException {
putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/calendar", calendar);
}

/**
* Request map position updates for the next ({@link count} * {@link interval}) number of seconds.
*
* @param count Number of updates
* @param interval Number of seconds between updates
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
public void requestPosition(int count, int interval) throws IndegoAuthenticationException, IndegoException {
postRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/requestPosition?count=" + count + "&interval=" + interval);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/
@NonNullByDefault
public class BoschIndegoConfiguration {
public String serialNumber = "";
public long refresh = 180;
public long stateActiveRefresh = 30;
public long cuttingTimeRefresh = 60;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants;
import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
import org.openhab.binding.boschindego.internal.handler.BoschIndegoHandler;
import org.openhab.binding.boschindego.internal.handler.BoschAccountHandler;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.ConsoleCommandCompleter;
import org.openhab.core.io.console.StringsCompleter;
Expand Down Expand Up @@ -61,9 +61,9 @@ public void execute(String[] args, Console console) {

for (Thing thing : thingRegistry.getAll()) {
ThingHandler thingHandler = thing.getHandler();
if (thingHandler instanceof BoschIndegoHandler indegoHandler) {
if (thingHandler instanceof BoschAccountHandler accountHandler) {
try {
indegoHandler.authorize(args[1]);
accountHandler.authorize(args[1]);
} catch (IndegoAuthenticationException e) {
console.println("Authorization error: " + e.getMessage());
}
Expand Down
Loading

0 comments on commit 4c064d3

Please sign in to comment.