diff --git a/CODEOWNERS b/CODEOWNERS index 4a68d285a178..21a86eb5fd90 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -294,6 +294,7 @@ /bundles/org.openhab.binding.rme/ @kgoderis /bundles/org.openhab.binding.robonect/ @reyem /bundles/org.openhab.binding.roku/ @mlobstein +/bundles/org.openhab.binding.romyrobot/ @wzbfyb @xeniter /bundles/org.openhab.binding.rotel/ @lolodomo /bundles/org.openhab.binding.russound/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.sagercaster/ @clinique diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index dadea0ba22ef..0206f5dd2b3c 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1456,6 +1456,11 @@ org.openhab.binding.roku ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.romyrobot + ${project.version} + org.openhab.addons.bundles org.openhab.binding.rotel diff --git a/bundles/org.openhab.binding.romyrobot/NOTICE b/bundles/org.openhab.binding.romyrobot/NOTICE new file mode 100644 index 000000000000..38d625e34923 --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.romyrobot/README.md b/bundles/org.openhab.binding.romyrobot/README.md new file mode 100644 index 000000000000..7372ab7f720e --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/README.md @@ -0,0 +1,95 @@ +# romyRobot Binding + +_Give some details about what this binding is meant for - a protocol, system, specific device._ + +_If possible, provide some resources like pictures (only PNG is supported currently), a video, etc. to give an impression of what can be done with this binding._ +_You can place such resources into a `doc` folder next to this README.md._ + +_Put each sentence in a separate line to improve readability of diffs._ + +## Supported Things + +_Please describe the different supported things / devices including their ThingTypeUID within this section._ +_Which different types are supported, which models were tested etc.?_ +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +- `bridge`: Short description of the Bridge, if any +- `sample`: Short description of the Thing with the ThingTypeUID `sample` + +## Discovery + +_Describe the available auto-discovery features here._ +_Mention for what it works and what needs to be kept in mind when using it._ + +## Binding Configuration + +_If your binding requires or supports general configuration settings, please create a folder ```cfg``` and place the configuration file ```.cfg``` inside it._ +_In this section, you should link to this file and provide some information about the options._ +_The file could e.g. look like:_ + +``` +# Configuration for the romyRobot Binding +# +# Default secret key for the pairing of the romyRobot Thing. +# It has to be between 10-40 (alphanumeric) characters. +# This may be changed by the user for security reasons. +secret=openHABSecret +``` + +_Note that it is planned to generate some part of this based on the information that is available within ```src/main/resources/OH-INF/binding``` of your binding._ + +_If your binding does not offer any generic configurations, you can remove this section completely._ + +## Thing Configuration + +_Describe what is needed to manually configure a thing, either through the UI or via a thing-file._ +_This should be mainly about its mandatory and optional configuration parameters._ + +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +### `sample` Thing Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|---------------------------------------|---------|----------|----------| +| hostname | text | Hostname or IP address of the device | N/A | yes | no | +| password | text | Password to access the device | N/A | yes | no | +| refreshInterval | integer | Interval the device is polled in sec. | 600 | no | yes | + +## Channels + +_Here you should provide information about available channel types, what their meaning is and how they can be used._ + +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +| Channel | Type | Read/Write | Description | +|---------|--------|------------|-----------------------------| +| control | Switch | RW | This is the control channel | + +## Full Example + +_Provide a full usage example based on textual configuration files._ +_*.things, *.items examples are mandatory as textual configuration is well used by many users._ +_*.sitemap examples are optional._ + +### Thing Configuration + +```java +Example thing configuration goes here. +``` + +### Item Configuration + +```java +Example item configuration goes here. +``` + +### Sitemap Configuration + +```perl +Optional Sitemap configuration goes here. +Remove this section, if not needed. +``` + +## Any custom content here! + +_Feel free to add additional sections for whatever you think should also be mentioned about your binding!_ diff --git a/bundles/org.openhab.binding.romyrobot/pom.xml b/bundles/org.openhab.binding.romyrobot/pom.xml new file mode 100644 index 000000000000..22da51eed713 --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.1.0-SNAPSHOT + + + org.openhab.binding.romyrobot + + openHAB Add-ons :: Bundles :: RomyRobot Binding + + diff --git a/bundles/org.openhab.binding.romyrobot/src/main/feature/feature.xml b/bundles/org.openhab.binding.romyrobot/src/main/feature/feature.xml new file mode 100644 index 000000000000..f4eb85b9ce2f --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.romyrobot/${project.version} + + diff --git a/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotBindingConstants.java b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotBindingConstants.java new file mode 100644 index 000000000000..3e9abd315c60 --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotBindingConstants.java @@ -0,0 +1,45 @@ +/** + * 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.romyrobot.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link romyRobotBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Bernhard Kreuz - Initial contribution + */ +@NonNullByDefault +public class RomyRobotBindingConstants { + + private static final String BINDING_ID = "romyrobot"; + + // List of all Thing Type UIDs + public static final ThingTypeUID ROMYROBOT_DEVICE = new ThingTypeUID(BINDING_ID, "aicu"); + + // List of all Channel ids + public static final String CHANNEL_FW_VERSION = "fwversion"; + public static final String CHANNEL_COMMAND = "command"; + public static final String CHANNEL_MODE = "mode"; + public static final String CHANNEL_ACTIVE_PUMP_VOLUME = "activepumpvolume"; + public static final String CHANNEL_STRATEGY = "strategy"; + public static final String CHANNEL_SUCTION_MODE = "suctionmode"; + public static final String CHANNEL_BATTERY = "battery"; + public static final String CHANNEL_CHARGING = "charging"; + public static final String CHANNEL_RSSI = "rssi"; + public static final String CHANNEL_POWER_STATUS = "powerstatus"; + public static final String CHANNEL_SELECTED_MAP = "selectedmap"; + public static final String CHANNEL_AVAILABLE_MAPS_JSON = "availablemapsjson"; +} diff --git a/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotConfiguration.java b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotConfiguration.java new file mode 100644 index 000000000000..7ea7f52f9d34 --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotConfiguration.java @@ -0,0 +1,33 @@ +/** + * 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.romyrobot.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link RomyRobotConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bernhard Kreuz - Initial contribution + */ +@NonNullByDefault +public class RomyRobotConfiguration { + + /** + * Sample configuration parameters. Replace with your own. + */ + public String hostname = ""; + public String password = ""; + public int refreshInterval = 600; + public int port = 8080; + public int timeout = 5; +} diff --git a/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotHandler.java b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotHandler.java new file mode 100644 index 000000000000..8992e3323c65 --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotHandler.java @@ -0,0 +1,165 @@ +/** + * 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.romyrobot.internal; + +import static org.openhab.binding.romyrobot.internal.RomyRobotBindingConstants.*; +import static org.openhab.core.library.unit.Units.PERCENT; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.validation.constraints.NotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.romyrobot.internal.api.RomyApi; +import org.openhab.binding.romyrobot.internal.api.RomyApiFactory; +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.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.StateOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link RomyRobotHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Bernhard Kreuz - Initial contribution + */ +@NonNullByDefault +public class RomyRobotHandler extends BaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(RomyRobotHandler.class); + + private RomyRobotConfiguration config; + private @Nullable ScheduledFuture pollingJob; + private RomyApi romyDevice; + private RomyApiFactory apiFactory; + private RomyRobotStateDescriptionOptionsProvider stateDescriptionProvider; + + public RomyRobotHandler(Thing thing, @NotNull RomyApiFactory apiFactory, + RomyRobotStateDescriptionOptionsProvider stateDescriptionProvider) throws Exception { + super(thing); + this.apiFactory = apiFactory; + this.stateDescriptionProvider = stateDescriptionProvider; + config = getConfigAs(RomyRobotConfiguration.class); + romyDevice = setupAPI(apiFactory); + } + + @Override + public void handleCommand(@NotNull ChannelUID channelUID, @NotNull Command command) { + if (command instanceof RefreshType) { + try { + getRomyApi().refresh(); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "RomyRobot refresh threw exception."); + } + } + if (CHANNEL_STRATEGY.equals(channelUID.getId())) { + updateState(CHANNEL_STRATEGY, new StringType(command.toString())); + try { + getRomyApi().setStrategy(command.toString()); + } catch (Exception e) { + logger.error("error updating strategy: {}", e.getMessage()); + } + } else if (CHANNEL_SUCTION_MODE.equals(channelUID.getId())) { + updateState(CHANNEL_SUCTION_MODE, new StringType(command.toString())); + try { + getRomyApi().setSuctionMode(command.toString()); + } catch (Exception e) { + logger.error("error updating suctionmode: {}", e.getMessage()); + } + } else if (CHANNEL_COMMAND.equals(channelUID.getId())) { + updateState(CHANNEL_COMMAND, new StringType(command.toString())); + try { + getRomyApi().executeCommand(command.toString()); + } catch (Exception e) { + logger.error("error executing command against RomyRobot", e); + } + } + } + + @Override + public void initialize() { + config = getConfigAs(RomyRobotConfiguration.class); + updateStatus(ThingStatus.UNKNOWN); + pollingJob = scheduler.scheduleWithFixedDelay(this::refreshVacuum, 2, config.refreshInterval, TimeUnit.SECONDS); + } + + public void refreshVacuum() { + try { + getRomyApi().refresh(); + updateStatus(ThingStatus.ONLINE); + this.updateChannels(getRomyApi()); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Could not sync status with RomyRobot, check your robot is unlocked to unlock it please provide password, you can find it under the dustbin (look for QR Code)" + + e.getMessage()); + } + } + + private void updateChannels(RomyApi device) { + updateState(CHANNEL_FW_VERSION, StringType.valueOf(device.getFirmwareVersion())); + updateState(CHANNEL_MODE, StringType.valueOf(device.getModeString())); + updateState(CHANNEL_ACTIVE_PUMP_VOLUME, StringType.valueOf(device.getActivePumpVolume())); + updateState(CHANNEL_BATTERY, QuantityType.valueOf(device.getBatteryLevel(), PERCENT)); + updateState(CHANNEL_CHARGING, StringType.valueOf(device.getChargingStatus())); + updateState(CHANNEL_POWER_STATUS, StringType.valueOf(device.getPowerStatus())); + updateState(CHANNEL_RSSI, new DecimalType(device.getRssi())); + updateState(CHANNEL_AVAILABLE_MAPS_JSON, StringType.valueOf(device.getAvailableMapsJson())); + updateMapsList(device.getAvailableMaps()); + } + + public void updateMapsList(HashMap maps) { + logger.trace("RomyRobot updating maps list with {} options", maps.size()); + List options = new ArrayList<>(); + for (String key : maps.keySet()) { + options.add(new StateOption(key, maps.get(key))); + } + stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SELECTED_MAP), options); + } + + private RomyApi setupAPI(RomyApiFactory apiFactory) throws Exception { + logger.debug("Initializing RomyRobot with config (Hostname: {}, Port: {}, Refresh: {}, Timeout {}).", + config.hostname, config.port, config.refreshInterval, config.timeout); + // hack: + logger.error("Initializing RomyRobot with config (Hostname: {}, Port: {}, Refresh: {}, Timeout {}).", + config.hostname, config.port, config.refreshInterval, config.timeout); + + return apiFactory.getHttpApi(config); + } + + private RomyApi getRomyApi() throws Exception { + romyDevice = apiFactory.getHttpApi(config); + return romyDevice; + } + + @Override + public void dispose() { + final ScheduledFuture pollingJob = this.pollingJob; + if (pollingJob != null) { + pollingJob.cancel(true); + this.pollingJob = null; + } + } +} diff --git a/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotHandlerFactory.java b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotHandlerFactory.java new file mode 100644 index 000000000000..4d3924311a89 --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotHandlerFactory.java @@ -0,0 +1,74 @@ +/** + * 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.romyrobot.internal; + +import static org.openhab.binding.romyrobot.internal.RomyRobotBindingConstants.ROMYROBOT_DEVICE; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.romyrobot.internal.api.RomyApiFactory; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link RomyRobotHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Bernhard Kreuz - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.romyrobot", service = ThingHandlerFactory.class) +public class RomyRobotHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(ROMYROBOT_DEVICE); + private RomyApiFactory apiFactory; + private RomyRobotStateDescriptionOptionsProvider stateDescriptionProvider; + private final Logger logger = LoggerFactory.getLogger(RomyRobotHandlerFactory.class); + + @Activate + public RomyRobotHandlerFactory(@Reference RomyApiFactory apiFactory, + @Reference RomyRobotStateDescriptionOptionsProvider stateDescriptionProvider) { + this.apiFactory = apiFactory; + this.stateDescriptionProvider = stateDescriptionProvider; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (ROMYROBOT_DEVICE.equals(thingTypeUID)) { + try { + return new RomyRobotHandler(thing, apiFactory, stateDescriptionProvider); + } catch (Exception e) { + logger.error("could not create handler {}", e.getMessage()); + } + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotStateDescriptionOptionsProvider.java b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotStateDescriptionOptionsProvider.java new file mode 100644 index 000000000000..8959a14015ce --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/RomyRobotStateDescriptionOptionsProvider.java @@ -0,0 +1,43 @@ +/** + * 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.romyrobot.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Dynamic provider of state options while leaving other state description fields as original. + * + * @author Gregory Moyer - Initial contribution + * @author Mark Hilbush - Adapted to squeezebox binding + */ +@Component(service = { DynamicStateDescriptionProvider.class, RomyRobotStateDescriptionOptionsProvider.class }) +@NonNullByDefault +public class RomyRobotStateDescriptionOptionsProvider extends BaseDynamicStateDescriptionProvider { + + @Activate + public RomyRobotStateDescriptionOptionsProvider(final @Reference EventPublisher eventPublisher, // + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, // + final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.eventPublisher = eventPublisher; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; + } +} diff --git a/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/api/RomyApi.java b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/api/RomyApi.java new file mode 100644 index 000000000000..bffa08468d18 --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/api/RomyApi.java @@ -0,0 +1,179 @@ +/** + * 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.romyrobot.internal.api; + +import java.util.HashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link RomyApi} interface defines the functions which are + * controllable on the Romy API interface. + * + * @author Bernhard Kreuz - Initial contribution + */ +@NonNullByDefault +public interface RomyApi { + + /** + * get robots firmwware version and name + * is available when robots http interface is locked + * + * @throws Exception + * + * @throws CommunicationApiException + * @throws UnauthorizedApiException + */ + void refreshID() throws Exception; + + /** + * get robots api protocol version + * is available when robots http interface is locked + * + * @throws Exception + * + * @throws CommunicationApiException + * @throws UnauthorizedApiException + */ + void refreshProtocolVersion() throws Exception; + + /** + * Sends all the GET requests and stores/cache the responses for use by the API to prevent the need for multiple + * requests. + * + * @throws Exception + * + * @throws CommunicationApiException + * @throws UnauthorizedApiException + */ + void refresh() throws Exception; + + /** + * + * @return Firmware Version of robot + */ + @Nullable + String getFirmwareVersion(); + + /** + * + * @return Firmware Version of robot + */ + @Nullable + String getName(); + + /** + * + * @return Status / Mode robot is currently in + */ + @Nullable + String getModeString(); + + /** + * + * @return currently set pump volume + */ + @Nullable + String getActivePumpVolume(); + + /** + * + * @param volume the pump volume used on next start + */ + void setActivePumpVolume(String volume); + + /** + * + * @return cleaning strategy + */ + @Nullable + String getStrategy(); + + /** + * + * @param strategy cleaning strategy + */ + void setStrategy(String strategy); + + /** + * + * @return suction mode, see thing xml for details + */ + @Nullable + String getSuctionMode(); + + /** + * + * @param suctionMode suction mode to be used for next start + */ + void setSuctionMode(String suctionMode); + + /** + * + * @return current battery level + */ + int getBatteryLevel(); + + /** + * + * @return weither the vacuum is charging + */ + @Nullable + String getChargingStatus(); + + /** + * + * @return WiFi rssi + */ + int getRssi(); + + /** + * + * @return current power status of the vacuum + */ + @Nullable + String getPowerStatus(); + + /** + * + * @return a String listing the available maps + */ + HashMap getAvailableMaps(); + + /** + * + * @return a String listing the available maps + */ + String getAvailableMapsJson(); + + /** + * + * @return Minor Interface Version + */ + int getProtocolVersionMinor(); + + /** + * + * @return Major Interface Version + */ + int getProtocolVersionMajor(); + + /** + * + * @param command command to execute + * @throws Exception + */ + void executeCommand(String command) throws Exception; +} diff --git a/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/api/RomyApiFactory.java b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/api/RomyApiFactory.java new file mode 100644 index 000000000000..9ef0defe1fce --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/api/RomyApiFactory.java @@ -0,0 +1,58 @@ +/** + * 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.romyrobot.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.romyrobot.internal.RomyRobotConfiguration; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link RomyApiFactory} class is used for creating instances of + * the Romy API classes to interact with the RomyRobot HTTP API. + * + * @author Bernhard Kreuz - Initial contribution + */ +@Component(service = RomyApiFactory.class) +@NonNullByDefault +public class RomyApiFactory { + private HttpClient httpClient; + + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Activate + public RomyApiFactory(@Reference HttpClientFactory httpClientFactory) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + } + + public RomyApi getHttpApi(RomyRobotConfiguration config) throws Exception { + int version = -1; + RomyApi lowestSupportedApi = new RomyApiV6(this.httpClient, config); + try { + version = lowestSupportedApi.getProtocolVersionMajor(); + } catch (Exception exp) { + logger.error("Problem fetching the firmware version from RomyRobot: {}", exp.getMessage()); + } + // will start to make sense once a breaking API Version > 6 is introduced + if (version >= 6) { + return lowestSupportedApi; + } else { + return lowestSupportedApi; + } + } +} diff --git a/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/api/RomyApiV6.java b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/api/RomyApiV6.java new file mode 100644 index 000000000000..695e49fabbe0 --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/api/RomyApiV6.java @@ -0,0 +1,374 @@ +/** + * 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.romyrobot.internal.api; + +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +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.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.romyrobot.internal.RomyRobotConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * The {@link RomyApiV6} interface defines the functions which are + * controllable on the Romy API interface Version 6. + * + * @author Bernhard Kreuz - Initial contribution + */ +@NonNullByDefault +public class RomyApiV6 implements RomyApi { + + private String hostname; + private RomyRobotConfiguration config; + protected HttpRequestSender http; + private @Nullable String firmwareVersion; + private @Nullable String name; + private @Nullable String mode; + private @Nullable String activePumpVolume; + private @Nullable String charging; + private int batteryLevel; + private @Nullable String powerStatus; + private String mapsJson = ""; + private int rssi; + private @Nullable String strategy; + private @Nullable String suctionMode; + private @Nullable String selectedMap; + + // that was the newest version when this code was written + private int protocolVersionMajor = 6; + private int protocolVersionMinor = 49; + + private HashMap availableMaps = new HashMap(); + private static final String CMD_GET_ROBOT_ID = "get/robot_id"; + private static final String CMD_GET_STATUS = "get/status"; + private static final String CMD_GET_MAPS = "get/maps"; + private static final String CMD_GET_WIFI_STATUS = "get/wifi_status"; + private static final String CMD_GET_POWER_STATUS = "get/power_status"; + private static final String CMD_GET_PROTOCOL_VERSION = "get/protocol_version"; + + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + public RomyApiV6(final HttpClient httpClient, final RomyRobotConfiguration config) { + this.config = config; + if (config.hostname.startsWith("http://") || config.hostname.startsWith("https://")) { + this.hostname = config.hostname; + } else { + this.hostname = "http://" + config.hostname; + } + + this.http = new HttpRequestSender(httpClient); + } + + /** + * Returns the hostname and port formatted URL as a String. + * + * @return String representation of the OpenSprinkler API URL. + */ + protected String getBaseUrl() { + return hostname + ":" + config.port + "/"; + } + + @Override + public void refreshID() throws Exception { + String returnContent = http.sendHttpGet(getBaseUrl() + CMD_GET_ROBOT_ID, null); + JsonNode jsonNode = new ObjectMapper().readTree(returnContent); + firmwareVersion = jsonNode.get("firmware").asText(); + if (firmwareVersion == null) { + logger.error("There was a problem in the HTTP communication: firmware was empty."); + } + name = jsonNode.get("name").asText(); + } + + @Override + public void refreshProtocolVersion() throws Exception { + String returnContent = http.sendHttpGet(getBaseUrl() + CMD_GET_PROTOCOL_VERSION, null); + JsonNode jsonNode = new ObjectMapper().readTree(returnContent); + protocolVersionMajor = jsonNode.get("version_major").intValue(); + protocolVersionMinor = jsonNode.get("version_minor").intValue(); + } + + @Override + public void refresh() throws Exception { + String returnContent = http.sendHttpGet(getBaseUrl() + CMD_GET_POWER_STATUS, null); + powerStatus = new ObjectMapper().readTree(returnContent).get("power_status").asText(); + + returnContent = http.sendHttpGet(getBaseUrl() + CMD_GET_STATUS, null); + JsonNode jsonNode = new ObjectMapper().readTree(returnContent); + mode = jsonNode.get("mode").asText(); + activePumpVolume = jsonNode.get("active_pump_volume").asText(); + charging = jsonNode.get("charging").asText(); + batteryLevel = jsonNode.get("battery_level").asInt(); + + returnContent = http.sendHttpGet(getBaseUrl() + CMD_GET_WIFI_STATUS, null); + jsonNode = new ObjectMapper().readTree(returnContent); + rssi = jsonNode.get("rssi").asInt(); + + mapsJson = http.sendHttpGet(getBaseUrl() + CMD_GET_MAPS, null); + parseMaps(mapsJson); + } + + private void parseMaps(String jsonString) throws JsonMappingException, JsonProcessingException { + JsonNode node = new ObjectMapper().readTree(jsonString); + JsonNode maps = node.get("maps"); + if (maps != null && maps.textValue() != null && !maps.textValue().isBlank() && maps.isArray()) { + availableMaps.clear(); + for (final JsonNode field : maps) { + String value = field.get("map_meta_data").textValue(); + String key = field.get("map_id").asInt() + ""; + String permanentFlag = field.get("permanent_flag").textValue(); + if ("true".equalsIgnoreCase(permanentFlag)) { + availableMaps.put(key, value); + } + } + if (availableMaps.size() == 1 || selectedMap == null) { + selectedMap = availableMaps.values().iterator().next(); + } + } else { + logger.warn("ROMY has no maps yet, please start a explore to create one!"); + } + } + + @Override + public @Nullable String getFirmwareVersion() { + return firmwareVersion; + } + + @Override + public @Nullable String getName() { + return name; + } + + @Override + public @Nullable String getModeString() { + return mode; + } + + @Override + public @Nullable String getActivePumpVolume() { + return activePumpVolume; + } + + @Override + public void setActivePumpVolume(String volume) { + this.activePumpVolume = volume; + } + + @Override + public @Nullable String getStrategy() { + return strategy; + } + + @Override + public void setStrategy(String strategy) { + this.strategy = strategy; + } + + @Override + public @Nullable String getSuctionMode() { + return suctionMode; + } + + @Override + public void setSuctionMode(String suctionMode) { + this.suctionMode = suctionMode; + } + + @Override + public int getBatteryLevel() { + return batteryLevel; + } + + @Override + public @Nullable String getChargingStatus() { + return charging; + } + + @Override + public int getRssi() { + return rssi; + } + + @Override + public @Nullable String getPowerStatus() { + return powerStatus; + } + + @Override + public String getAvailableMapsJson() { + return mapsJson; + } + + /** + * This class contains helper methods for communicating HTTP GET and HTTP POST + * requests. + * + * @author Chris Graham - Initial contribution + * @author Florian Schmidt - Reduce visibility of Http communication to Api + */ + protected class HttpRequestSender { + private final HttpClient httpClient; + + public HttpRequestSender(HttpClient httpClient) { + this.httpClient = httpClient; + } + + /** + * Given a URL and a set parameters, send a HTTP GET request to the URL location + * created by the URL and parameters. + * + * @param url The URL to send a GET request to. + * @param urlParameters List of parameters to use in the URL for the GET + * request. Null if no parameters. + * @return String contents of the response for the GET request. + * @throws Exception + */ + public String sendHttpGet(String url, @Nullable String urlParameters) throws Exception { + String location = null; + if (urlParameters != null) { + location = url + "?" + urlParameters; + } else { + location = url; + } + + logger.debug("sendHttpGet location:{}", location); + ContentResponse response; + try { + response = withGeneralProperties(httpClient.newRequest(location)) + .timeout(config.timeout, TimeUnit.SECONDS).method(HttpMethod.GET).send(); + } catch (Exception e) { + logger.error("Request to RomyRobot device failed: {}", e.getMessage()); + return ""; + } + + if (response.getStatus() == HttpStatus.FORBIDDEN_403) { + // forbiden, looks like http interface is locked, try to unlock it + // ------------------------------------------------------------------ + URL netUrl = new URL(url); + try { + logger.info( + "looks like http interface is locked, try to unlock it now with password from config..."); + String unlock = netUrl.getProtocol() + "://" + netUrl.getHost() + ":" + netUrl.getPort() + + "/set/unlock_http?pass=" + config.password; + response = withGeneralProperties(httpClient.newRequest(unlock)) + .timeout(config.timeout, TimeUnit.SECONDS).method(HttpMethod.GET).send(); + } catch (Exception e) { + logger.error("Request to unlock RomyRobot device with password {} failed: {}", config.password, + e.getMessage()); + } + + // send request again after unlocking + // ------------------------------------- + try { + response = withGeneralProperties(httpClient.newRequest(location)) + .timeout(config.timeout, TimeUnit.SECONDS).method(HttpMethod.GET).send(); + } catch (Exception e) { + logger.error("Request to RomyRobot device failed: {}", e.getMessage()); + } + } + if (response.getStatus() != HttpStatus.OK_200) { + logger.error("Error sending HTTP GET request to {}. Got response code: {}", url, response.getStatus()); + } + return response.getContentAsString(); + } + + private Request withGeneralProperties(Request request) { + return request; + } + + /** + * Given a URL and a set parameters, send a HTTP POST request to the URL + * location created by the URL and parameters. + * + * @param url The URL to send a POST request to. + * @param urlParameters List of parameters to use in the URL for the POST + * request. Null if no parameters. + * @return String contents of the response for the POST request. + * @throws Exception + */ + public String sendHttpPost(String url, String urlParameters) throws Exception { + ContentResponse response; + try { + response = withGeneralProperties(httpClient.newRequest(url)).timeout(config.timeout, TimeUnit.SECONDS) + .method(HttpMethod.POST).content(new StringContentProvider(urlParameters)).send(); + } catch (Exception e) { + logger.error("Request to RomyRobot device failed: {}", e.getMessage()); + return ""; + } + if (response.getStatus() != HttpStatus.OK_200) { + logger.error("Error sending HTTP POST request to {}. Got response code: {}", url, response.getStatus()); + } + return response.getContentAsString(); + } + } + + @Override + public void executeCommand(String command) throws Exception { + if ("REFRESH".equalsIgnoreCase(command)) { + return; + } + // String query = "/$" + command; + String query = null; + List params = new ArrayList(); + if ("clean_start_or_continue".equalsIgnoreCase(command) || "clean_all".equalsIgnoreCase(command) + || "clean_spot".equalsIgnoreCase(command) || "clean_map".equalsIgnoreCase(command)) { + if (suctionMode != null && !"REFRESH".equals(suctionMode)) { + params.add("cleaning_parameter_set=" + suctionMode); + } + if (strategy != null && !"REFRESH".equals(strategy)) { + params.add("cleaning_strategy_mode=" + strategy); + } + if (activePumpVolume != null) { + params.add("pump_volume=" + activePumpVolume); + } + if (params.isEmpty()) { + query = String.join("&", params); + } + } else if ("redo_explore".equalsIgnoreCase(command) || "clean_map".equalsIgnoreCase(command)) { + params.add("map_id" + selectedMap); + } + String url = getBaseUrl() + "set/" + command; + logger.info("executing RomyRobot command: {} at url {}", query, url); + http.sendHttpGet(url, query); + } + + @Override + public HashMap getAvailableMaps() { + return availableMaps; + } + + @Override + public int getProtocolVersionMajor() { + return protocolVersionMajor; + } + + @Override + public int getProtocolVersionMinor() { + return protocolVersionMinor; + } +} diff --git a/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/discovery/RomyRobotMDNSDiscoveryParticipant.java b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/discovery/RomyRobotMDNSDiscoveryParticipant.java new file mode 100644 index 000000000000..ce25e41ddf74 --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/java/org/openhab/binding/romyrobot/internal/discovery/RomyRobotMDNSDiscoveryParticipant.java @@ -0,0 +1,128 @@ +/** + * 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.romyrobot.internal.discovery; + +import static org.openhab.binding.romyrobot.internal.RomyRobotBindingConstants.*; +import static org.openhab.core.thing.Thing.*; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.jmdns.ServiceInfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.romyrobot.internal.RomyRobotConfiguration; +import org.openhab.binding.romyrobot.internal.api.RomyApi; +import org.openhab.binding.romyrobot.internal.api.RomyApiFactory; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a discovery participant which finds RomyRobots on the local network + * through their mDNS announcements. + * + * @author Manuel Dipolt - Initial contribution + * + */ +@NonNullByDefault +@Component +public class RomyRobotMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant { + + private final Logger logger = LoggerFactory.getLogger(RomyRobotMDNSDiscoveryParticipant.class); + + private RomyApiFactory apiFactory; + + @Activate + public RomyRobotMDNSDiscoveryParticipant( + @Reference RomyApiFactory apiFactory /* @Reference HttpClientFactory httpClientFactory */) { + logger.debug("Activating ROMY Discovery service"); + this.apiFactory = apiFactory; + } + + @Override + public String getServiceType() { + return "_aicu-http._tcp.local."; + } + + @Override + public Set getSupportedThingTypeUIDs() { + Set supportedThingTypeUIDs = new HashSet<>(); + supportedThingTypeUIDs.add(ROMYROBOT_DEVICE); + return supportedThingTypeUIDs; + } + + @Override + public @Nullable ThingUID getThingUID(ServiceInfo service) { + return new ThingUID(ROMYROBOT_DEVICE, service.getName()); + } + + @Override + public @Nullable DiscoveryResult createResult(ServiceInfo service) { + final ThingUID uid = getThingUID(service); + if (uid == null) { + logger.error("uid is null!"); + return null; + } + + logger.info("Discovered ROMY vacuum cleaner robot: {}", service); + + // get IP address + String address = ""; + String robotName = ""; + String robotLabel = ""; + String robotUniqeId = service.getName(); + String[] hostAddresses = service.getHostAddresses(); + + if (hostAddresses.length == 0) { + logger.error("hostAddresses is empty!"); + return null; + } + + logger.debug("hostAddresses: {}", Arrays.toString(hostAddresses)); + address = hostAddresses[0]; + logger.debug("address: {}", address); + + try { + RomyRobotConfiguration config = new RomyRobotConfiguration(); + config.hostname = address; + RomyApi romyDevice = apiFactory.getHttpApi(config); + romyDevice.refreshID(); + romyDevice.refreshProtocolVersion(); + robotName = romyDevice.getName(); + logger.debug("New ROMY with the name: {}", robotName); + } catch (Exception e) { + logger.error("Error setting up ROMY api: {}", e.getMessage()); + return null; + } + + robotLabel = String.format("%s (%s)", robotName, address); + + DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperty(PROPERTY_SERIAL_NUMBER, robotUniqeId) + .withProperty("hostname", address).withLabel(robotLabel) + .withRepresentationProperty(PROPERTY_SERIAL_NUMBER).build(); + + logger.debug("DiscoveryResult: {}", result); + + return result; + } +} diff --git a/bundles/org.openhab.binding.romyrobot/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.romyrobot/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 000000000000..01701d0dfcee --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,10 @@ + + + + binding + romyRobot Binding + This is the binding for romyRobot. + + diff --git a/bundles/org.openhab.binding.romyrobot/src/main/resources/OH-INF/i18n/romyrobot.properties b/bundles/org.openhab.binding.romyrobot/src/main/resources/OH-INF/i18n/romyrobot.properties new file mode 100644 index 000000000000..0c2f44015a92 --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/resources/OH-INF/i18n/romyrobot.properties @@ -0,0 +1,3 @@ +# FIXME: please add all English translations to this file so the texts can be translated using Crowdin +# FIXME: to generate the content of this file run: mvn i18n:generate-default-translations +# FIXME: see also: https://www.openhab.org/docs/developer/utils/i18n.html diff --git a/bundles/org.openhab.binding.romyrobot/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.romyrobot/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 000000000000..b4803c649ca8 --- /dev/null +++ b/bundles/org.openhab.binding.romyrobot/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,169 @@ + + + + + + A RomyRobot vacuum robot + CleaningRobot + + + + + + + + + + + + + + + + + + + network-address + + The host name or IP address of your ROMY robot + + + password + + Please take note that the local http interface is locked per default you have to unlock it with the + code you find under the dustbin. + + + + Port of the RomyRobot Web API interface. + 8080 + true + + + + Interval the device is polled in sec. + 180 + true + + + + Specifies the connection timeout in seconds. + 5 + true + + + + Specifies the connection timeout in seconds. + 5 + true + + + + + + String + + Firmware version installed on vacuum + + + + String + + Command to execute + + + + + + + + + + + + + + String + + Intensitiy of water usage + + + + + + + + + + + String + + Choose an available cleaning strategy + + + + + + + + + + + String + + Choose an available suction mode + + + + + + + + + + + + String + + Robots current status + + + + Number + + Battery charge percentage + + + + String + + Indicates the vacuums charging status + + + + Number + + Wi-Fi signal strength + + + + String + + Robots current power status + + + + String + + selected map if multiple maps are stored, used on commands + + + String + + Information about availabler maps. Useful for composing commands + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index bc0ef858994b..0c0096df4e0d 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -326,6 +326,7 @@ org.openhab.binding.rme org.openhab.binding.robonect org.openhab.binding.roku + org.openhab.binding.romyrobot org.openhab.binding.rotel org.openhab.binding.russound org.openhab.binding.sagercaster